java依赖混乱存在的问题与解决方案
作者:GuangHui
呈现的形态
- 1.缺少防腐层,业务与外部接口耦合;
- 2.业务代码中出现具体实现类。
存在的问题与解决方案
缺少防腐层,会让请求对象传导到业务代码中,造成了业务与外部接口的耦合,也就是业务依赖了一个外部通信协议。一般来说,业务的稳定性要比外部接口高,这种反向的依赖就会让业务一直无法稳定下来,继而在日后带来更多的问题。解决方案自然就是引入一个防腐层,将业务和接口隔离开来。
业务代码中出现具体的实现类,实际上是违反了依赖倒置原则。因为违反了依赖倒置原则,业务代码也就不可避免地受到具体实现的影响,也就造成了业务代码的不稳定。识别一段代码是否属于业务,我们不妨问一下,看把它换成其它的东西,是否影响业务。解决这种坏味道就是引入一个模型,将业务与具体的实现隔离开来。
编程规则
依赖倒置原则,即高层模块不应依赖于底层模块,二者应依赖于抽象。抽象不应该依赖细节,细节应该依赖于抽象。
记住一句话
代码应该向着稳定性的方向依赖。
缺少防腐层
@PostMapping("/books") public NewBookResponse createBook(final NewBookRequest request) { boolean result = this.service.createBook(request); ... }
按照通常的架构设计原则,service 层属于我们的核心业务,而 controller层属于接口。二者相较而言,核心业务的重要程度更高一些,所以,它的稳定程度也应该更高一些。同样的业务,我们可以用 REST 的方式对外提供,也可以用 RPC 的方式对外提供。
NewBookRequest穿越了两层,职责不清晰的同时,也存在破坏核心业务层稳定性的风险。为此,我们只要再引入一个模型就可以破解这个问题。
class NewBookParameter { ... } class NewBookRequest { public NewBookParameters toNewBookRequest() { ... } } @PostMapping("/books") public NewBookResponse createBook(final NewBookRequest request) { boolean result = this.service.createBook(request.toNewBookParameter()); ... }
通过增加一个模型,我们就破解了依赖关系上的纠结。也许你会说,虽然它们成了两个类,但是,它们两个应该长得一模一样吧。这算不算是一种重复呢?但我的问题是,它们两个为什么要一样呢?有了两层不同的参数,我们就可以给不同层次上的模型以不同的约定了。
比如,对于 controller 层的请求对象,因为它的主要作用是传输,所以,一般来说,我们约定请求对象的字段主要是基本类型。而 service 的参数对象,因为它已经是核心业务的一部分,就需要全部转化为业务对象。举个例子,比如,同样表示价格,在请求对象中,我们可以是一个 double 类型,而在业务参数对象中,它应该是 Price 类型。
业务代码里的具体实现
@Task public void sendBook() { try { this.service.sendBook(); } catch (Throwable t) { this.feishuSender.send(new SendFailure(t))); throw t; } }
上面的不符合设计原则,它违反了依赖倒置原则。高层模块不应依赖于低层模块,二者应依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。
上面这段代码,在一段业务处理中出现了一个具体的实现,也就是这里的 feishuSender。你需要知道,业务代码中任何与业务无关的东西都是潜在的坏味道。
在这里,飞书肯定不是业务的一部分,它只是当前选择的一个具体实现。换言之,是否选择飞书,与团队当前的状态是相关的,如果哪一天团队切换即时通信软件,这个实现就需要换掉。
识别一个东西是业务的一部分,还是一个可以替换的实现,我们不妨问问自己,如果不用它,是否还有其它的选择?就像这里,飞书是可以被其它即时通信软件替换的。
另外,常见的中间件,比如,Kafka、Redis、MongoDB 等等,通常也都是一个具体的实现,其它中间件都可以把它替换掉。所以,它们在业务代码里出现,那一定就是一个坏味道了。
既然我们已经知道了,这些具体的东西是一种坏味道,那该怎么解决呢?你可以引入一个模型,也就是这个具体实现所要扮演的角色,通过它,将业务和具体的实现隔离开来。
interface FailureSender { void send(SendFailure failure); } class FeishuFailureSenderS implements FailureSender { ... }
这里我们通过引入一个 FailureSender,业务层只依赖于这个 FailureSender 的接口就好,而具体的飞书实现可以通过依赖注入的方式注入进去。
依赖关系是软件开发中非常重要的一个东西,然而,很多程序员在写代码的时候,由于开发习惯的原因,常常会忽略掉依赖关系这件事本身。
现在已经有一些工具,可以保证我们在写代码的时候,不会出现严重破坏依赖关系的情况,比如,像前面那种 service 层调用 resource 层的代码。
在 Java 世界里,我们就可以用 ArchUnit 来保证这一切。看名字就不难发现,它是把这种架构层面的检查做成了单元测试,下面就是这样的一个单元测试:
@Test public void should_follow_arch_rule() { JavaClasses clazz = new ClassFileImporter().importPackages("..."); ArchRule rule = layeredArchitecture() .layer("Resource").definedBy("..resource..") .layer("Service").definedBy("..service..") .whereLayer("Resource").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Resource"); rule.check(clazz); }
在这里,我们定义了两个层,分别是 Resource 层和 Service 层,而且我们要求 Resource 层的代码不能被其它层访问,而 Service 层的代码只能由 Resource 层方法访问。这就是我们的架构规则,一旦代码里有违反这个架构规则的代码,这个测试就会失败,问题也就会暴露出来。
以上就是java依赖混乱存在的问题与解决方案的详细内容,更多关于java依赖混乱的资料请关注脚本之家其它相关文章!