美文网首页
代码坏味道:依赖混乱

代码坏味道:依赖混乱

作者: GuangHui | 来源:发表于2022-09-23 19:40 被阅读0次

    坏味道呈现的形态:

    • 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 层方法访问。这就是我们的架构规则,一旦代码里有违反这个架构规则的代码,这个测试就会失败,问题也就会暴露出来。

    相关文章

      网友评论

          本文标题:代码坏味道:依赖混乱

          本文链接:https://www.haomeiwen.com/subject/qiinortx.html