如何遵循“低耦合”设计原则?

2022-07-24 16:53:46 浏览数 (2)

类的设计需要遵循“高内聚低耦合”的设计原则(或者说“高内聚、松耦合”)。什么是高内聚和低耦合:

  • 高内聚:内聚是对软件系统中元素职责相关性和集中度的度量。如果元素具有高度相关的职责,除了这些职责内的任务,没有其它过多的工作,那么该元素就具有高内聚性。
  • 低耦合:耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据

某种程度上理解,高内聚低耦合也是单一职责原则迪米特法则的另一种体现。

结合项目上开发的经历,要将一个类设计为高内聚和低耦合的类,难的是实现和后期应对临时需求和corner case而增加的改动。当然,这也是没有良好设计种下的苦果。这种痛苦,会在某次增加feature时感受到,也可能是在重构的时候。总之,虽迟但到

下面先看一个违背低耦合设计原则的示例。考虑如下一个示例:按钮控制电灯开关。

代码语言:javascript复制
#include <iostream>
 
#define ON  true
#define OFF false
 
class Lamp
{
public:
    void on() {
        // ...
    }
    void off() {
        // ...
    }
};
 
class Button
{
public:
    Button(Lamp& lamp): mLamp(lamp){}
    void touch() {
        if (mState == ON) {
            mState = OFF;
            mLamp.off();
        }
        else {
            mState = ON;
            mLamp.on();
        }
    }
private:
    Lamp& mLamp;
    bool mState{OFF};
};

这段代码是可以正常运行的,乍一看也没什么问题:

  • 创建电灯Lamp的实例。
  • 再创建Button的实例,并将Lamp对象的引用传给Button构造函数。
  • 接下来只需调用Button对象的touch()函数,即可控制电灯的开关状态。

这有什么问题吗?当然有问题——将Lamp对象的引用传给Button。这样Button内部就知道了lamp。但问题是,我们去买一个按钮开关的时候,按钮开关知道电灯吗?按钮开关还可以控制洗衣机、风扇、中央空调……那我们是不是要把所有这些可用按钮开关控制的对象的引用都传给按钮开关呢?

Button需要知道这么多吗?显然不需要,不需要将Button与其控制的具体对象耦合起来。这显然违背了低耦合的设计原则。Button只需要控制一个可用开关控制的对象即可,至于这个对象具体是什么,Button无需了解。电灯也好,电视、风扇、空调、洗衣机也罢,它们同属于可用开关控制的对象,都具有“打开”和“关闭”两种状态,那么就让它们继承自一个共同的基类即可:

扩展地,如果一个Button可以同时控制多个对象,那么就在Button内部维护一个列表吧。

Jungle回去再翻看了下命令模式:作为程序员的你,必须要知道命令模式!

同样,代码实例中举的是开关控制点灯和电风扇的场景:

我原以为之前的设计会违背“低耦合”原则:Button内部直接控制Lamp和Fan。这就需要将Lamp对象和Fan对象以指针或引用(或者其他形式)传递给Button,然后Button内部操作这些对象。如果真这么设计,就犯了上面第二点中提到的错误。

这里的设计也符合“低耦合”的设计原则,只将一个抽象的Command传递给Button。Button并不知道Command细节,即,Button和Command并没有耦合在一起。如果要增加被控制的对象,那么直接继承自Command就可以了,无序改动已有的代码,这也符合开闭原则。

上面的示例也可以看出,降低耦合的办法之一是,引入抽象类(或者接口)。使对象依赖于抽象,而不是依赖于具体。这其实也是依赖倒转原则的要求。

第二种办法是,使用并严格遵循一些既有的设计模式,比如中介者模式、命令模式、适配器模式等。因为设计模式本身就是为了解决某类重复出现的问题而出现的一套成功或有效的解决方案。Just follow it!

另一种办法是引入分层。某种程度上来说,设计模式的使用,或者抽象类的引入,就是分层的一种体现。通过引入中间层,引入抽象层,来降低类与类之间的耦合。不过,引入分层,往往需要初期对项目有完整的规划。

温故而知新,就说这么多~

0 人点赞