原文自国外技术社区dzone,作者为 John Vester,传送门
想象下这个场景...
你现在有机会帮客户去设计一套新的 API 了。上头的指令(或者你自己的决定)是需要使用 SpringBoot 及其衍生的依赖注入。你开始撸起袖子开干 — 对新项目充满期待和感到兴奋。
作为一个开发者或者是设计者,你想让你的服务尽可能地保持专一性。这代表着如果你设计一个 CustomerService
类,在它里面就不会涉及到一些小工具方法的编写了。于是你决定设计一个 WidgetService
类来帮你处理这些事情。没错,这个看起来很符合逻辑。
渐渐地,你的应用变得更加丰富。换句话说,通过简单地遍历下文件系统的类名列表,你就可以轻松理解所有服务类的含义了。你甚至不需要打开类的接口来查看就可以明确每个类的责任。
为了更浅显易懂,下面有一个例子:
services/
├─ AccountService.java
├─ CustomerService.java
├─ ...
├─ SalesService.java
├─ UserService.java
└─ WidgetService.java
嗯,你非常自豪。这个应用维护起来真的是非常简单呀!
然后事情变得有点复杂
想象下上述类似的类列表中的项超过五个的时候,或许在你结束整个初期设计后发现有五十个或者将近一百个类文件在里面。
现在,你开始需要为每个服务加入跨服务功能了举个例子,在应用中需要设计一个全方位报告功能?
你的 ReportService
类中设计为接口,而这个接口的实现可能就像下面这样:
@Service
public class ReportServiceImpl implements ReportService {
private AccountService accountService;
private CustomerService customerService;
private DService dService;
private EService dService;
private FService fService;
private HService hService;
private IService iService;
private JService jService;
private MoreServicesService moreServicesService;
private SoMuchTypingService soMuchTyingService;
private SalesService salesService;
private UserService userService;
private WidgetService widgetService;
public ReportServiceImpl(@Lazy AccountService accountService, @Lazy CustomerService customerService,
@Lazy DService dService, @Lazy EService eService, @Lazy FService fService,
@Lazy GService gService, @Lazy HService hService, @Lazy IService idService,
@Lazy JService jService, @Lazy MoreServicesService moreServicesService,
@Lazy SooMuchTypingService soMuchTypingService, @Lazy SalesService salesService
@Lazy UserService userService, @Lazy WidgetService widgetService) {
this.accountService = accountService;
this.customerService = customerService;
this.dService = dService;
this.eService = eService;
this.fService = fService;
this.gService = gService;
this.hService = hService;
this.iService = iService;
this.jService = jdService;
this.moreServicesService = moreServicesService;
this.soMuchTypingService = soMuchTypingService;
this.salesService = salesService;
this.userService = userService;
this.widgetService = widgetService;
}
/**
* {@inheritDoc}
*/
@Override
public List<SomeObject> someReportingServiceMethod() {
// 走你 ....
}
}
在上述的例子中,有相当多的服务依赖被注入进去。其实它们并不需要都被设计为服务类,事实上,大部分是可以被定义成 JpaRepository
API 的扩展接口。
然而,一些静态分析工具(像 CheckStyle)会提示出在这个类中的某些方法或者构造器有过多的参数。CheckStyle 在这里的最多参数量的默认值是7,是 ReportServiceImpl()
构造方法中所需参数的一般。
如果 CheckStyle 是作为 CI/CD pineline 中默认检查的一环的话,这个 ReportServiceImpl
的构建必然会失败。
方法论证
在例子的 ReportService
中,假设有业务要求需要将应用中的所有服务面(不管是 service 还是 repository)注入至内的话,以便为客户提供预想的结果。
每个服务都具有各自的细粒度并且专一于提供和服务名相符的功能,完成这一点是需要花费大量的时间的。同样,还需要确保 CustomService
不会去做 WidgetService
所做的事这样类似的设计。
对于报告这种服务,是需要去注入每个服务,来确保在报告中能够获得所有需要的数据。作为一个开发者,我可能会含糊其辞地说 "在报告服务类当中会包含所有报告数据" — 但是我仍然不得不注入 repository 接口来获取实际的数据。
跳出常规思维来想想,另一种替代方法是,将压力转交到给客户端中,让它们去做每个服务 API 的调用并且在它们那里构建各自的数据报告。虽然这方案确实可行,但还需要考虑以下的一些事项:
- 在多个客户端应用中可能会有重复的报告逻辑,从而增加了产生错误数据的可能性
- 检索所需的信息所发起多个 API 调用会造成一定程度的网络阻塞
- 制定更适合(后端)服务端处理的客户端处理逻辑对性能的影响
最后,关于报告的需求(或者任何驱动)是必须被提供的,这样才可以满足应用的功能需求。在这中情况下,告诉产品这个需求需要改动或者移除。原因是对于分析工具,有某个默认属性的值不被满足,导致不能完成构建,这在开发中是不被接受的,整个工程会受到影响。
结论
思考下上述的例子,我觉得满足应用需求的最佳实践方法是在服务类的公共构造方法中注入尽量少的依赖。
尽管上述所提及的注入 如此多的依赖并不多见,但我认为这在实际情况是可能发生的。但我不清楚的是,当在单一的一个服务类中注入将近一百个 service 或者 repository 的时候所产生的影响会怎样。会不会有注入失败的情况发生呢?
我对这个场景非常感兴趣,如果顺利的话,我已经开始着手于此了,去构建一个需要注入大量依赖来完成应用的需求的例子,来验证下我的思路。
那么下次再见!