耦合和解耦

耦合

耦合代表着模块间的关联关系。

大多数耦合产生于业务逻辑的需求,是为我们所需要的。对于这部分耦合应该保留甚至强化:如将隐式的变为显示的,松散的变为内聚。具体做法:有使用类似字典的数据结构将双方绑定、使用全局变量关联数据等。

对于少部分不合理的耦合(模块间不合理的依赖关系),如依赖于外部模块不稳定的行为、模块间双向依赖、修改全局变量而影响其他模块等行为,我们需要对其进行解耦以加强代码的稳定性和可扩展、可以移植性。

解耦原则

  1. 让模块保持完整且独立

    具体表现为:

    • 对内有完整的逻辑,所依赖的外部资源尽量是不变量
    • 对外提供稳定的特性

    实例:

    一个人要读书:

    Person person = new Person();
    person.readBook(book);
    

    而其中readBook操作需要戴眼镜

    class Person {
        // ...
        void readBook(Book book) {
            wearGlasses(this.myGlasses);
            read(book);
        }
    }
    

    此时如果person没有眼镜(this.myGlassesnull),person.readBook(book)则会出现异常

    优化1:属性注入

    在调用readBook前对person中的创建眼镜

    person.myGlasses = new Glasses();
    person.readBook(book);
    

    但这就给readBook方法加入了一个隐式要求:调用前先需要对myGlasses赋值,给程序员带来了额外的记忆负担。

    优化2:通过构造函数注入

    Person构造函数中加一个glasses参数

    class Person {
    	Person(Glasses glasses) {
            this.myGlasses = glasses;
        }
    }
    

    每次创建Person对象时都会强制要求传入一个glasses,减轻了程序员的记忆要求。但是对于Person中的许多方法glasses并不是必须的,专为了可能并不会被使用的readBook方法就要求Person在初始化时需要传入一个glasses,十分浪费且不方便。自己方法的特殊需求应该由方法自己解决。

    优化3:通过普通成员函数注入

    将构造方法中的参数移动到readBook自己的参数列表中。

    class Person {
        //...
        void readBook(Book book, Glasses glasses) {
            wearGlasses(glasses);
            read(book);
        }
    }
    

    在调用时则使用

    person.readBook(book, new Glasses());
    

    这样解决了优化2中的诸多不便,但是每次调用都new一个眼镜有些浪费,且依赖于外部的眼镜对象可能导致不稳定行为。

    优化4:封装注入

    person每次可以使用自己持有的眼镜

    person.readBook(book, this.myGlasses)
    

    但是回到最开始的问题,this.myGlasses可能为空。我们可以通过封装一个get方法来解决这个问题。

    class Person {
        //...
        Glasses getGlasses() {
            if (this.myGlasses == null)
                this.myGlasses = new Glasses();
            return this.myGlasses;
        }
        void readBook(Book book) {
            wearGlasses(getGlasses());
            read(book);
        }
    }
    

    这样每次readBook都会复用同一副眼镜而且不会影响person中其他函数。

  2. 保持模块间的连接稳定

    当外部需求方法变化时,连接模块间的接口应该保持不变,让变化发生在模块内部而不是对外的接口上。这需要架构师的提前预判和设计,改变接口的代价要远大于保持变化在模块内部的代价。