前言
首先明确一下, 这里所说的系统模块划分, 是针对 client,service,common 这样的技术划分, 而不是针对具体业务的模块划分. 避免由于歧义, 造成你的时间浪费.
直接原因
公司内部某技术团队, 在引用我们系统的 client 包时, 启动失败.
失败原因是由于 client 下有一个 cache 相关的依赖, 其注入失败导致的.
然后, 就发出了这样一个疑问: 我只是希望使用一个 hsf 接口, 为什么还要引入诸如缓存, web 处理工具等不相关的东西.
这也就自然地引出了前辈对我的一句教导: 对外的 client 需要尽可能地轻便.
很明显, 我们原有的 client 太重了, 包含了对外的 RPC 接口, 相关模型(如 xxxDTO), 工具包等等.
可能有人就要提出, 直接 RPC + 模型一个包, 其它内容一个包不就 OK 了嘛?
问题真的就这么简单嘛?
根本原因
其实出现上述问题, 是因为在系统设计之初, 并没有深入思考 client 包的定位, 以及日后可能遇到的情况.
这也就导致了今天这样的局面, 所幸目前的外部引用并不多, 更多是内部引用. 及时调整, 推广新的依赖, 与相关规范为时不晚.
常见模块拆分
先说说我以前的模块拆分. 最早的拆分是每个业务模块主要拆分为:
xxx-service: 具体业务实现模块.
xxx-client: 对外提供的 RPC 接口模块.
xxx-common: 对外的工具, 以及模型.
这种拆分方式, 是我早期从一份微服务教程中看到的. 优点是简单明了, 调用方可选择性地选择需要的模块引入.
至于一些通用的组件, 如统一返回格式 (如 ServerResponse,RtObject), 则放在了最早的核心(功能核心, 但内容很少) 模块上.
后来, 认为这样并不合适, 不应该将通用组件放在一个业务模块上. 所以建立了一个 base 模块, 用来将通用的组件, 如工具, 统一返回格式等都放入其中.
另外, 将每个服务都有的 xxx-common 模块给取消了. 将其中的模型, 放入了 xxx-client, 毕竟是外部调用需要的. 将其中的工具, 根据需要进行拆分:
base: 多个服务都会使用的.
xxx-service: 只有这个服务本身使用
xxx-client: 有限的服务使用, 并且往往是服务提供方和服务调用方都要使用. 但是往往这种情况, 大多是由于接口设计存在问题导致的. 所以多为过渡方案.
上述这个方案, 也就是我在负责某物联网项目时采用的最终模块划分方式.
在当时的业务下, 该方案的优点是模块清晰, 较为简洁, 并且尽可能满足了迪米特原则(可以参考《阿里 Java 开发手册相关实践》). 缺点则是需要一定的技术水平, 对组件的功能域认识清晰. 并且需要有一定的设计思考与能力(如上述工具拆分的第三点 - xxx-client, 明白为什么这是设计缺陷导致, 并能够解决).
新的问题
那么, 既然上述的方案挺不错的, 为什么不复用到现在的项目呢?
因为业务变了, 导致应用场景变了. 而这也带来了新的问题, 新的考虑角度.
原先的物联网业务规模并不大, 所以依赖也较为简单, 也并不需要进行依赖的封装等, 所以针对主要是 client 的内 / 外这一维度考虑的.
但是现有的业务场景, 由于规模较大, 模块依赖层级较多, 导致上层模块引入过多的依赖. 如有一个缓存模块, 依赖 tair-starter(一个封装的 key-value 的存储), 然后日志模块依赖该缓存模块(进行性能优化), 紧接着日志模块作为一个通用模块, 被放入了 common 模块中. 依赖链路如下:
调用方 -> common 模块 -> 日志模块 -> 缓存模块 -> tair-starter 依赖
但是有的调用方表示, 根本就不需要日志模块, 却引入了 tair-starter 这一重依赖(starter 作为封装, 都比较重), 甚至由于 tair-starter 的内部依赖与自身原有依赖冲突, 得去排查依赖, 进行 exclude.
但是同时, 也有的调用方, 系统通过 rich 客户端, 达到性能优化等目标.
所以, 现有的业务场景除了需要考虑 client 的内 / 外这一维度, 还需要考虑 client 的 pool/rich 这一维度.
可能有的小伙伴, 看到这里有点晕乎乎的, 这两个维度考量的核心在哪里?
内 / 外, 考虑的是按照内外这条线, 尽量将 client 设计得简洁, 避免给调用方引入无用依赖.
而 pool/rich, 考虑的是性能, 用户的使用成本 (是否开箱即用) 等.
最终解决方案
最终的解决方案是对外提供 3+n
xxx-client(1 个): 所有外部系统引用都需要的内容, 如统一返回格式等.
xxx-yyy-client(n 个): 对具体业务依赖的引用, 进行了二次拆分. 如 xxx-order-client(这里是用订单提花那你一下, 大家理解意思就 OK).
xxx-pool-client(1 个): 系统引用所需要的基本依赖, 如 Lindorm 的依赖等.
xxx-rich-client(1 个): 系统引用所需要的依赖与对应 starter, 如一些自定义的自动装载 starter(不需要用户进行配置).
这个方案, 换个思路, 理解也简单.
我们提供相关的能力, 具体如何选择, 交给调用方决定.
其实, 讨论中还提到了 BOM 方案(通过 DependentManagement 进行 jar 包版本管理). 不过分析后, 我们认为 BOM 方案更适合那些依赖集比较稳定的 client, 如一些中间件. 而我们目前的业务系统, 还在快速发展, 所以并不适用.
总结
简单来说, 直接从用户需求考虑(这里的用户就是调用方):
外部依赖:
额外引入的依赖尽可能地少, 最好只引入二方依赖(我们提供的 jar), 不引入第三方依赖.
引入的二方依赖不 "夹带" 私货(如二方 jar 引入了一堆大第三方依赖).
自动配置:
可以傻瓜式使用. 如引入对应的 starter 依赖, 就可以自动装配对应默认配置.
也可以自定义配置. 用户可以在自定义配置, 并不用引入无效的配置(因为 starter 经常引入不需要的依赖).
性能:
可以通过 starter, 提供一定的封装, 保证一定的性能(如接口缓存, 请求合并等).
可以自定义实现基础功能. 因为有些人并不放心功能封装(虽然只是少数, 但是稳定性前辈提出的).
补充
这里补充一点, 我对讨论中一个问题的回答, 这里提一下.
有人提到工具类, 应该如何划分. 因为有的工具类, 是不依赖于服务状态的, 如 CookieUtil 进行 Cookie 处理. 有的工具类, 是依赖于服务状态的, 如 RedisUtil 包含 RedisPool 状态, 直连 Redis, 处理 Redis 请求与响应.
其实这里有两个细节:
工具应该按照上面的方式进行划分为两种. 单一模块系统, 不依赖服务状态的工具往往置于 util 包, 依赖服务状态的工具往往置于 common 包中. 这里还有一个明显的区分点: 前者的方法可以设为 static, 而后者并不能(因为依赖于 new 出来的状态).
依赖于状态的工具类, 其实是一种拆分不完全的体现. 如 RedisUtil, 可以拆分为连接状态管理的 RedisPool 与请求响应处理的 RedisUitl. 两者的组合方式有很多, 取决于使用者的需要, 以后有机会写一篇相关的博客. 不过, 我希望大家记住 面向接口编程的原则.
愿与诸君共进步.
来源: https://www.cnblogs.com/Tiancheng-Duan/p/12776537.html