Bootstrap

设计模式(4)——七大原则

四、day4

在学习工厂模式的时候我们了解到了开闭原则,但究竟什么是开闭原则,它有什么用?设计模式还有没有其他的设计原则?在本节中将一一回答这些问题。

设计模式常用七大原则:

  1. 单一职责原则
  2. 接口隔离原则
  3. 里氏替换原则
  4. 依赖倒转(倒置)原则
  5. 迪米特法则
  6. 合成复用原则
  7. 开闭原则

1. 单一职责原则

C++面向对象三大特性之一的封装指的就是将单一事物抽象出来组合成一个类,所以我们在设计类的时候每个类中处理的是单一事物而不是某些事物的集合。

设计模式中所谓的单一职责原则,就是对一个类而言,应该仅有一个引起它变化的原因,其实就是将这个类所承担的职责单一化。如果一个类承担的职责过多,就等于把这些职责耦合到了一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致设计变得脆弱,当变化发生时,设计会遭受到意想不到的破坏。

软件设计真正要做的事情就是,发现根据需求发现职责,并把这些职责进行分离,添加新的类,给当前类减负,越是这样项目才越容易维护。

在这里插入图片描述

如上 UML图所示,我们不能讲生活辅导和学业指导均放在一个人的身上,他可能会因重负而崩溃,我们应该将学生工作分为两大职责并分别交给两个人,这样学生管理工作才能仅仅有条,不出错。

2. 接口隔离原则

接口隔离原则(ISP)提倡将过于复杂、庞大的接口拆分成小而精的接口,让每个接口只包含客户真正需要的方法,避免多余的内容。换句话说,接口的方法越少越好,但又要足够专注、实用。同时,在拆分接口时,需要遵循单一职责原则,确保每个接口的职责清晰明确,而不是盲目地将接口拆得过于零碎。

在这里插入图片描述

图片来源:https://blog.csdn.net/zhengzhb/article/details/7296921

如上图所示,类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现(类B继承抽象接口类I,实现接口I中的所有纯虚函数,C++中接口类是仅包含纯虚函数的类)。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法(因为C++中抽象类中的纯虚函数不会在基类中定义,所以派生类中必须定义基类中所有的纯虚函数)。如下段代码所示:

// 定义接口 I
class I {
public:
    virtual void method1() = 0;
    virtual void method2() = 0;
    virtual void method3() = 0;
    virtual void method4() = 0;
    virtual void method5() = 0;
    virtual ~I() {}
};

// 类 A 依赖接口 I
class A {
public:
    void depend1(I* i) {
        i->method1();
    }
    void depend2(I* i) {
        i->method2();
    }
    void depend3(I* i) {
        i->method3();
    }
};

// 类 B 实现接口 I
class B : public I {
public:
    void method1() override {
        cout << "类 B 实现接口 I 的方法1" << endl;
    }
    void method2() override {
        cout << "类 B 实现接口 I 的方法2" << endl;
    }
    void method3() override {
        cout << "类 B 实现接口 I 的方法3" << endl;
    }
	//对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
	//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    void method4() override {}
    void method5() override {}
};

// 类 C 依赖接口 I
class C {
public:
    void depend1(I* i) {
        i->method1();
    }
    void depend2(I* i) {
        i->method4();
    }
    void depend3(I* i) {
        i->method5();
    }
};

// 类 D 实现接口 I
class D : public I {
public:
    void method1() override {
        cout << "类 D 实现接口 I 的方法1" << endl;
    }
    // method2 和 method3 对于类 D 来说不是必需的,但仍需实现
    void method2() override {}
    void method3() override {}
    void method4() override {
        cout << "类 D 实现接口 I 的方法4" << endl;
    }
    void method5() override {
        cout << "类 D 实现接口 I 的方法5" << endl;
    }
};

// 客户端代码
int main() {
    A a;
    B b;
    a.depend1(&b);
    a.depend2(&b);
    a.depend3(&b);

    C c;
    D d;
    c.depend1(&d);
    c.depend2(&d);
    c.depend3(&d);

    return 0;
}

在代码中,接口过于臃肿,因为接口类 I 是纯虚函数类,所以派生类在继承接口类I时必须定义所有的纯虚函数,即使我们并用不到该函数。虚函数保证了多态的实现,这里我们在类 A/C 中接受基类 I 的指针,由于虚函数表的存在我们对基类指针进行方法的调用会自动转到已实现的派生类上。

如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如下图所示:

在这里插入图片描述

图片来源:https://blog.csdn.net/zhengzhb/article/details/7296921
// 定义接口 I1
class I1 {
public:
    virtual void method1() = 0;
    virtual ~I1() {}
};

// 定义接口 I2
class I2 {
public:
    virtual void method2() = 0;
    virtual void method3() = 0;
    virtual ~I2() {}
};

// 定义接口 I3
class I3 {
public:
    virtual void method4() = 0;
    virtual void method5() = 0;
    virtual ~I3() {}
};

// 类 A 依赖接口 I1 和 I2
class A {
public:
    void depend1(I1* i) {
        i->method1();
    }
    void depend2(I2* i) {
        i->method2();
    }
    void depend3(I2* i) {
        i->method3();
    }
};

// 类 B 实现接口 I1 和 I2
class B : public I1, public I2 {
public:
    void method1() override {
        cout << "类 B 实现接口 I1 的方法1" << endl;
    }
    void method2() override {
        cout << "类 B 实现接口 I2 的方法2" << endl;
    }
    void method3() override {
        cout << "类 B 实现接口 I2 的方法3" << endl;
    }
};

// 类 C 依赖接口 I1 和 I3
class C {
public:
    void depend1(I1* i) {
        i->method1();
    }
    void depend2(I3* i) {
        i->method4();
    }
    void depend3(I3* i) {
        i->method5();
    }
};

// 类 D 实现接口 I1 和 I3
class D : public I1, public I3 {
public:
    void method1() override {
        cout << "类 D 实现接口 I1 的方法1" << endl;
    }
    void method4() override {
        cout << "类 D 实现接口 I3 的方法4" << endl;
    }
    void method5() override {
        cout << "类 D 实现接口 I3 的方法5" << endl;
    }
};

// 客户端代码
int main() {
    A a;
    B b;
    a.depend1(&b);
    a.depend2(&b);
    a.depend3(&b);

    C c;
    D d;
    c.depend1(&d);
    c.depend2(&d);
    c.depend3(&d);
    return 0;
}

接口隔离原则的核心思想是:为每个类建立专用的小型接口,而不是设计一个庞大而通用的接口供所有类使用。也就是说,接口应该尽量精简,方法越少越好。通过将一个复杂的接口拆分成多个小接口,不同的类可以只依赖它们真正需要的部分,这样既提高了灵活性,也避免了因为接口过于庞大而导致的冗余和复杂性。比如本文的例子中,将一个大接口拆分为三个小接口,就是接口隔离原则的典型应用。在程序设计中,依赖多个小接口比依赖一个大接口更灵活,同时也有助于减少变更的影响,提高系统的维护性和扩展性。

很多人可能会觉得接口隔离原则和单一职责原则很相似,但它们其实有本质区别。单一职责原则强调的是“职责”的独立性,主要约束的是类的设计,同时也会涉及接口和方法,更多针对程序的具体实现和细节。而接口隔离原则则侧重于**“接口依赖”的分离**,主要约束接口的设计,强调的是程序整体框架的抽象和灵活性。因此,二者关注点不同,作用范围也不完全相同。

3. 里氏替换原则

里氏替换原则(Liskov Substitution Principle)要求所有引用基类的地方必须能透明地使用其子类的对象。也就是在继承关系中,子类尽量不要重写父类(父类非抽象类)的方法。继承实际上让两个类耦合性增强了,特别是运行多态比较频繁的时,整个继承体系的复用性会比较差。

  • 问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
  • 解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

举例说明,假设我们需要完成一个两数相减的功能,由类A来负责:

class A {
public:
    int func1(int a, int b) {
        return a - b;
    }
};

int main() {
    A a;
    std::cout << "100 - 50 = " << a.func1(100, 50) << std::endl;
    std::cout << "100 - 80 = " << a.func1(100, 80) << std::endl;
    return 0;
}

后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:1)两数相减;2)两数相加,然后再加100。 由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:

// 基类 A
class A {
public:
    virtual int func1(int a, int b) {
        return a - b;
    }
};

// 派生类 B
class B : public A {
public:
    // 重写 func1 方法
    int func1(int a, int b) override {
        return a + b;
    }
    // 新增 func2 方法
    int func2(int a, int b) {
        return func1(a, b) + 100;
    }
};

int main() {
    B b;
    std::cout << "100 - 50 = " << b.func1(100, 50) << std::endl; // 150
    std::cout << "100 - 80 = " << b.func1(100, 80) << std::endl; // 180
    std::cout << "100 + 20 + 100 = " << b.func2(100, 20) << std::endl; // 220
    return 0;
}

我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的 func1 方法,造成所有运行相减功能的代码全部调用了类B重写后的方法(如果基类的函数被声明为virtual,而派生类定义了一个函数名、参数列表和返回类型完全相同的函数,那么派生类的函数将覆盖基类的函数,如果仍旧想要调用基类被覆盖的函数,需要基类类名加作用域运算符::显式调用),造成原本运行正常的功能出现了错误。

在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类(父类非抽象类)原有的功能。它包含以下4层含义:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

4. 依赖倒转(倒置)原则

依赖倒置原则有以下 2 个含义:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节,细节应该依赖抽象。

解释一下这两句话中的一些抽象概念:

  • 高层模块:可以理解为上层应用,就是业务层的实现

  • 低层模块:可以理解为底层接口,比如封装好的API、动态库等

  • 抽象:指的就是抽象类或者接口,在C++中没有接口,只有抽象类。

举一个例子:

大聪明的项目组接了一个新项目,低层使用的是MySql的数据库接口,高层基于这套接口对数据库表进行了添删查改,实现了对业务层数据的处理。而后由于某些原因,要存储到数据库的数据量暴增,所以更换了Oracle数据库,由于低层的数据库接口变了,高层代码的数据库操作部分是直接调用了低层的接口,因此也需要进行对应的修改,无法实现对高层代码的直接复用,大聪明欲哭无泪。

解决方式很简单:高层不再直接依赖于底层的数据库接口,而是依赖于接口 IMySqlOracle的底层接口继承接口 **I **并实现,高层通过接口 I 间接与MySqlOracle的底层接口发生联系,则会大大降低修改高层的几率。如下图所示:

在这里插入图片描述

图片来源:https://subingwen.cn/design-patterns/three-principles/#%E9%87%8C%E6%B0%8F%E4%BB%A3%E6%8D%A2%E5%8E%9F%E5%88%99
  • 抽象类中提供的接口是固定不变的

  • 低层模块是抽象类的子类,继承了抽象类的接口,并且可以重写这些接口的行为

  • 高层模块想要实现某些功能,调用的是抽象类中的函数接口,并且是通过抽象类的父类指针引用其子类的实例对象(用子类类型替换父类类型),这样就实现了多态。

基于依赖倒转原则将项目的结构换成上图的这种模式之后,低层模块发生变化,对应高层模块是没有任何影响的(说白了,依赖倒转原则就是对多态的典型应用)。

5. 迪米特法则

迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle,LKP)。表示一个类应该对自己需要耦合或调用的类知道的最少,你的内部是如何复杂都和我没有关系,那是你的事情,我就知道你提供那么多 public 方法,我就调用这么多,其他的我一概不关心。

迪米特法则还有一个更简单的定义:只与直接的朋友通信。

什么是直接的朋友?

每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。

其中,我们称出现在成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类(方法体内部的类)则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

迪米特法则的目的就是低耦合,它包含 4 层含义:

1、只和朋友交流

也就是说符合迪米特法则的类中的方法,应该不能出现非朋友类,不能和陌生人有交流。

举个例子,如果类 A 通过类 B 调用了类 C 的方法,这种间接调用就算是和“陌生人”(类 C)交流了。为了减少耦合,A 不应该直接接触类 C。

2、朋友之间也是有距离的

人与人之间是有距离的,类与类之间也有距离,不能太过亲密。

  • 问题来源:比如 A 类有 3 个方法,被 B 类的一个方法 m1 全部调用了,这样一来就会有一个问题,当 A 类修改方法的名称时,B 类也要修改 m1 方法,这就是耦合太紧了。

  • 解决办法

    • m1 方法转移到 A 类中去,让 B 类调用 A 的 m1 方法,这样就是高内聚低耦合了。
    • 同时 A 类中的 3 个方法可以设置成私有的,因为只有他自己调用,只需要将 m1 设置成 public 就可以了。
  • 关键思想:公开的属性和方法越多,修改带来的影响越大,所以类应该“羞涩”一点,尽量隐藏内部实现。

3、是自己的就是自己的

如果一个方法逻辑完全属于本类,不需要和别的类有交互关系,也不会给本类带来负面影响,那么这个方法就应该放在本类中。不要为了图方便,把属于自己的功能分散到其他类里。

举例:
假设有一个功能是计算两点之间的距离,它完全是数学上的逻辑,与任何具体的类无关。我们在设计时应该把这个功能封装到一个独立的工具类中,比如 MathUtils。但是,为了图方便,有人把这个功能拆分到其他与它无关的类中,比如:

  • 类 A:负责表示点的坐标;
  • 类 B:负责管理一组点;
  • 类 C:负责绘制点到屏幕上。

为了实现“计算距离”这个功能,开发者分别在类 A、类 B 和类 C 中各自实现了一部分逻辑,例如:

  • 在类 A 中定义了点的基本信息;
  • 在类 B 中处理两点之间的计算;
  • 在类 C 中实现了部分绘制与距离测量相关的功能。

问题来源:

  1. 功能分散:距离计算的逻辑分散在多个类中,没有统一的实现,维护起来困难。如果需要优化计算公式或修复一个 bug,所有相关类都需要改动。
  2. 增加耦合:这些类之间的关系变得复杂,不再独立。修改一个类可能会连带影响其他类。
  3. 复用性差:如果另一个地方需要类似的计算,还得重新实现,不能直接复用。

解决办法: 将“计算距离”的逻辑抽取成一个独立的工具类 MathUtils,这样任何需要计算距离的地方都可以直接调用,而不依赖具体的类。

6. 合成复用原则

合成复用原则(Composite Reuse Principle)就是是尽量使用合成/聚合的方式,而不是使用继承。因为继承会带来更紧密的耦合,子类会继承父类所有的属性和方法,可能导致不必要的功能冗余。而通过组合或聚合,可以更灵活地使用所需功能,减少类之间的依赖,提高代码的可维护性。

举例:

问题来源:类 A 提供一个通用功能(比如日志功能),类 B 需要使用这个功能,但 B 并不需要继承 A 的所有属性和方法。

解决方法

  • 我们可以换成依赖关系,以参数形式将类 A 传入类 B 中。
  • 也可以换成聚合关系,以属性和构造器形式。
  • 还可以换成组合关系,创建实例时构造。
// A类:提供日志功能
class Logger {
public:
    void log(const string& message) {
        cout << "Log: " << message << endl;
    }
};

// Application类:通过依赖关系使用Logger
class Application {
public:
    void run(Logger& logger) { // Logger通过参数传递
        logger.log("Application is running...");
        cout << "Application logic executed." << endl;
    }
};

// Application类:通过聚合关系使用Logger
class Application {
private:
    Logger* logger; // 使用指针表示聚合关系
public:
    Application(Logger* logger) : logger(logger) {} // 通过构造器注入Logger

    void run() {
        if (logger) {
            logger->log("Application is running...");
        }
        cout << "Application logic executed." << endl;
    }
};

// Application类:通过组合关系使用Logger
class Application {
private:
    Logger logger; // 通过组合关系使用Logger
public:
    void run() {
        logger.log("Application is running...");
        cout << "Application logic executed." << endl;
    }
};

7. 开闭原则

开放 – 封闭原则说的是软件实体(类、模块、函数等)可以扩展,但是不可以修改。也就是说对于扩展是开放的,对于修改是封闭的

  • 问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
  • 解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

其实,我们遵循设计模式前面6大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面6项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面六项原则遵守程度的“平均得分”,前面6项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面6项原则遵守的不好,则说明开闭原则遵守的不好。

开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

说到这里,再回想一下前面说的6项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;接口隔离原则告诉我们在设计接口的时候要精简单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;迪米特法和合成复用原则则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

最后说明一下如何去遵守这七个原则。对这七个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的七个设计原则也是一样,制定这七个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用一幅图来说明一下。

在这里插入图片描述

图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。

在这里插入图片描述

图片来源:https://blog.csdn.net/zhengzhb/article/details/7296944?spm=1001.2014.3001.5502

在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。

开闭原则摘抄自博主岸似达春绿的文章:设计模式六大原则(6):开闭原则

8. 类与类之间的关系

可参考文章:UML 类图 | 爱编程的大丙

我在这里简单的说一下:

  • 继承也叫作泛化(Generalization)

    • 在UML中,泛化关系用带空心三角形的实线来表示。
  • 关联(Assocition)关系是类与类之间最常见的一种关系,它是一种结构化的关系,表示一个对象与另一个对象之间有联系,如汽车和轮胎、师傅和徒弟、班级和学生等。

    • 在UML类图中,用(带接头或不带箭头的)实线连接有关联关系的类。
    • 在代码中,通常将一个类的对象作为另一个类的成员变量。
  • 聚合(Aggregation)关系表示整体与部分的关系。在聚合关系中,成员对象是整体的一部分,但是成员对象可以脱离整体对象独立存在(比如汽车与 引擎、轮胎、车灯;森林与 植物、动物、水、阳光)。

    • 在UML中,聚合关系用带空心菱形的直线表示。
    • 在代码中,对象以属性成员(指针)形式存在,通过构造函数或其他方式注入,但生命周期由外部控制。成员对象通常以构造方法、Setter方法的方式注入到整体对象之中,因为成员对象可以脱离整体对象独立存在。
  • 组合(Composition)关系也表示的是一种整体和部分的关系,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也不存在,整体对象和成员对象之间具有同生共死的关系(头和 嘴巴、鼻子、耳朵、眼睛)。

    • 在UML中组合关系用带实心菱形的直线表示。
    • 在代码中。对象以属性成员(非指针,类)形式存在,由类直接管理,生命周期与宿主类一致。通常在整体类的构造函数体中直接实例化成员类。
  • 依赖(Dependency)关系是一种使用关系,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系,大多数情况下依赖关系体现在某个类的方法使用另一个类的对象作为参数(驾驶员开车,需要将车对象作为参数传递给 Driver 类的drive()方法)。

    • 在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。
    • 在代码中,依赖关系由以下三点体现:1)将一个类的对象作为另一个类中方法的参数;2)在一个类的方法中将另一个类的对象作为其对象的局部变量;3)在一个类的方法中调用另一个类的静态方法。

    类之间的关系强弱顺序是这样的:继承(泛化) > 组合 > 聚合 > 关联 > 依赖

以下是关于继承、组合、聚合、关联和依赖根据关系强弱顺序的表格:


关系类型定义特点代码示例适用场景
继承 (Inheritance)子类继承父类,子类拥有父类的所有非私有成员,可以扩展和重写父类功能。强耦合,"是一个"的关系,父类改变会影响子类;代码复用性高但灵活性较低。class B : public A { };当类之间具有明显的“是一个”的关系,并需要复用父类代码时使用。
组合 (Composition)一个类完全拥有另一个类的实例,依赖类的生命周期与宿主类一致。强耦合,"整体-部分"的关系,宿主类销毁时组成部分也被销毁;组合对象由宿主类控制。class B {A* a;}B(){a = new A;}当一个类高度依赖另一个类的功能,并且组合对象是宿主类不可分割的一部分时使用。
聚合 (Aggregation)一个类部分拥有另一个类的实例,依赖类的生命周期独立于宿主类。中等耦合,"整体-部分"的关系,但部分可以脱离整体存在;组合对象的生命周期由外部控制。class B { A* a; };B(A* a) : a(a) { }当一个类需要长时间依赖另一个类,但需要由外部来管理生命周期时使用。
关联 (Association)一个类与另一个类通过某种逻辑关系相关联,但不拥有实例。松耦合,"知道一个"的关系,两个类之间通过某种关联完成通信,彼此独立但相关。class B {A a;}当类之间需要短暂交互时使用,比如方法调用,但不需要长期关联或共享实例。
依赖 (Dependency)一个类依赖另一个类的功能以完成自己的功能,通常是通过方法参数传递对象实例。最弱耦合,"使用一个"的关系,类之间的耦合性最低,通常是短暂的交互关系。class B { void func(A& a) { a.method(); } };B::func(A& a)当类之间仅需要临时调用另一个类的功能时使用。

以下示例代码展示了这五种关系的典型实现:

  1. 继承
class A {
public:
    void method() { cout << "A's method" << endl; }
};

class B : public A {
public:
    void method() { cout << "B's overridden method" << endl; }
};
  1. 组合
class A {
public:
    void method() { cout << "A's method" << endl; }
};

class B {
private:
    A* a; // A是B的一部分
public:
    B(){
        a = new A;
    }
};
  1. 聚合
class A {
public:
    void method() { cout << "A's method" << endl; }
};

class B {
private:
    A* a; // 聚合:B持有A的指针
public:
    B(A* a) : a(a) {}
    void callA() { a->method(); }
};
  1. 关联
class A
{
};

class B
{
private:
    A a;
};
  1. 依赖
class A {
public:
    void method() { cout << "A's method" << endl; }
};

class B {
public:
    void run(A& a) { a.method(); } // 依赖关系:通过参数调用A的方法
};

参考:

Java设计模式七大原则-CSDN博客

设计模式-七大原则(图解一目了然)-腾讯云开发者社区-腾讯云

设计模式——六大设计原则-腾讯云开发者社区-腾讯云

设计模式六大原则(4):接口隔离原则_类a通过接口i依赖类b代码-CSDN博客

设计模式六大原则(2):里氏替换原则-CSDN博客

设计模式六大原则(3):依赖倒置原则_依赖倒置原则的核心-CSDN博客

设计模式六大原则(6):开闭原则_架构设计六大原则-CSDN博客

岸似达春绿-CSDN博客

;