设计模式总结(一)

设计模式是在软件开发中常见的解决特定问题的可复用设计方案。它们提供了一种通用的方法来解决常见的设计问题,有助于使代码更加可维护、灵活和可扩展。在以前的时候做过一次总结,也就是下面这个笔记,这次将它分享给大家。

这里先介绍设计模式的七大原则:

单一职责原则(Single Responsibility Principle,SRP)

单一职责原则是面向对象设计中的一个重要原则,它指导着一个类应该只有一个引起变化的原因,或者说一个类应该只负责一组相关的功能。这意味着一个类应该专注于完成单一的任务,而不应该包含过多的责任。如果一个类承担了过多的责任,那么修改其中一个责任可能会影响到其他责任,从而导致代码的脆弱性和不稳定性。

例如,一个类既负责处理用户输入,又负责处理数据存储,违反了单一职责原则。因为用户输入和数据存储是两个不同的功能,应该分别由不同的类来处理。

以下是一个简单的 C++ 代码示例,演示了一个 ViolatingSRP 类违反了单一职责原则,以及如何通过拆分职责来符合该原则:

cpp 复制代码
#include <iostream>
#include <string>

// ViolatingSRP 违反了单一职责原则
class ViolatingSRP {
public:
    void processUserInput(const std::string& userInput) {
        // 处理用户输入的功能
        std::cout << "Processing user input: " << userInput << std::endl;
    }

    void saveDataToFile(const std::string& data) {
        // 保存数据到文件的功能
        std::cout << "Saving data to file: " << data << std::endl;
    }
};

// 通过拆分职责来符合单一职责原则
class UserInputProcessor {
public:
    void processUserInput(const std::string& userInput) {
        // 处理用户输入的功能
        std::cout << "Processing user input: " << userInput << std::endl;
    }
};

class DataSaver {
public:
    void saveDataToFile(const std::string& data) {
        // 保存数据到文件的功能
        std::cout << "Saving data to file: " << data << std::endl;
    }
};

int main() {
    // ViolatingSRP 违反了单一职责原则的类
    ViolatingSRP violatingSRP;
    violatingSRP.processUserInput("User input data");
    violatingSRP.saveDataToFile("Data to save");

    // 通过拆分职责来符合单一职责原则的类
    UserInputProcessor userInputProcessor;
    userInputProcessor.processUserInput("User input data");

    DataSaver dataSaver;
    dataSaver.saveDataToFile("Data to save");

    return 0;
}

在上面的示例中,ViolatingSRP 类负责处理用户输入和保存数据到文件两个不同的功能,违反了单一职责原则。而拆分职责后的 UserInputProcessor 类专门负责处理用户输入,DataSaver 类专门负责保存数据到文件,每个类都只负责一个功能,符合单一职责原则。

接口隔离原则(Interface Segregation Principle,ISP)

接口隔离原则要求接口应该小而专门,而不是大而笨重。该原则的核心思想是客户端不应该依赖它不需要的接口,一个类不应该被迫实现它用不到的接口。接口隔离原则旨在降低接口的耦合性,使得系统更加灵活、可维护和可扩展。

接口隔离原则主要有以下几个要点:

  1. 接口应该小而专门:接口应该只包含客户端需要的方法,不应该包含客户端不需要的方法。这样可以避免将不相关的方法暴露给客户端,减少了客户端的依赖。

  2. 接口设计应该符合客户端的需求:在设计接口时应该考虑到客户端的需求,确保接口提供的方法对客户端来说是有意义的、可理解的,并且只包含客户端需要的方法。

  3. 避免臃肿的接口:接口不应该包含大量的方法,而应该保持简洁。如果一个接口过于庞大,往往意味着它违反了单一职责原则。

  4. 接口隔离有助于解耦:通过将大接口拆分为多个小接口,可以降低模块之间的耦合度,提高系统的灵活性和可维护性。

以下是一个简单的 C++ 代码示例,演示了接口隔离原则的应用:

cpp 复制代码
#include <iostream>

// 违反接口隔离原则的示例
class Worker {
public:
    virtual void work() = 0; // 工作方法
    virtual void eat() = 0;  // 吃饭方法
    virtual void sleep() = 0; // 睡觉方法
};

// 符合接口隔离原则的示例
class IWorkable {
public:
    virtual void work() = 0; // 工作方法
};

class IEatable {
public:
    virtual void eat() = 0; // 吃饭方法
};

class ISleepable {
public:
    virtual void sleep() = 0; // 睡觉方法
};

// Worker 类只实现了工作相关的接口
class BetterWorker : public IWorkable {
public:
    void work() override {
        std::cout << "Working..." << std::endl;
    }
};

int main() {
    // 符合接口隔离原则的示例
    BetterWorker worker;
    worker.work();

    return 0;
}

在上面的示例中,Worker 类违反了接口隔离原则,因为它包含了工作、吃饭和睡觉三个不相关的方法。而在修正后的代码中,我们将接口拆分为 IWorkableIEatableISleepable 三个小接口,分别表示工作、吃饭和睡觉。这样可以使得类只需要实现自己需要的接口,避免了不必要的依赖。

依赖倒置原则(Dependency Inversion Principle,DIP)

依赖倒置原则强调高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。换句话说,高层模块和低层模块都应该依赖于抽象,而具体的实现细节则应该依赖于抽象而不是相反。

依赖倒置原则主要有以下几个要点:

  1. 高层模块和低层模块之间通过抽象进行通信:高层模块和低层模块都应该依赖于抽象,而不是直接依赖于具体的实现细节。这样可以降低模块之间的耦合度,提高系统的灵活性和可维护性。

  2. 抽象不应该依赖于具体实现细节:抽象应该定义清晰明确的接口,而不应该依赖于具体的实现细节。这样可以保持抽象的稳定性,避免因为具体实现的改变而影响到抽象。

  3. 具体实现细节应该依赖于抽象:具体的实现细节应该依赖于抽象定义的接口,而不应该依赖于其他具体实现细节。这样可以使得具体的实现细节可以灵活地替换和扩展,而不影响到其他部分。

依赖倒置原则通常通过依赖注入(Dependency Injection)来实现,即通过构造函数注入、方法参数注入或者接口注入等方式将依赖关系转移到外部管理。

以下是一个简单的 C++ 代码示例,演示了依赖倒置原则的应用:

cpp 复制代码
#include <iostream>

// 定义抽象接口
class IWorker {
public:
    virtual void work() = 0; // 工作方法
};

// 具体的工作类,实现了抽象接口
class Worker : public IWorker {
public:
    void work() override {
        std::cout << "Working..." << std::endl;
    }
};

// 高层模块,依赖于抽象接口
class Manager {
private:
    IWorker* worker; // 依赖注入

public:
    Manager(IWorker* w) : worker(w) {}

    void manage() {
        worker->work(); // 通过抽象接口调用工作方法
    }
};

int main() {
    // 创建具体的工作类对象
    Worker worker;

    // 创建高层模块对象,并通过构造函数注入具体的工作类对象
    Manager manager(&worker);

    // 调用高层模块的方法
    manager.manage();

    return 0;
}

在上面的示例中,Manager 类是一个高层模块,它依赖于抽象接口 IWorker 而不是具体的 Worker 类。通过构造函数注入的方式将具体的 Worker 对象注入到 Manager 类中,使得 Manager 类可以通过抽象接口调用工作方法,而不需要知道具体的实现细节。这样可以实现依赖倒置原则,降低了模块之间的耦合度。

里氏替换原则(Liskov Substitution Principle,LSP)

里氏替换原则由芭芭拉·利斯科夫(Barbara Liskov)提出。它是针对继承关系的一个原则,指导着子类应该能够替换掉父类并且程序仍然能够正确运行。换句话说,子类必须能够替换其父类并保持程序的逻辑正确性,而不需要修改原有的程序。

里氏替换原则主要有以下几个要点:

  1. 子类必须能够替换父类:任何基类可以出现的地方,子类一定可以出现,而且要能够替换掉基类并且程序仍然能够正确运行。

  2. 继承意味着 "is-a" 关系:子类应该继承父类的全部属性和方法,并且符合 "is-a" 关系,即子类对象应该可以被当作父类对象使用。

  3. 子类不应该重写父类的非抽象方法:子类可以通过重写父类的抽象方法来实现自己的业务逻辑,但是不应该重写父类的非抽象方法,这会破坏原有的程序逻辑。

  4. 里氏替换原则是实现多态性的基础:通过遵循里氏替换原则,可以实现多态性,提高程序的灵活性和可扩展性。

以下是一个简单的 C++ 代码示例,演示了里氏替换原则的应用:

cpp 复制代码
#include <iostream>

// 父类
class Shape {
public:
    virtual double area() const = 0; // 抽象方法:计算面积
};

// 子类:矩形
class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }
};

// 子类:正方形
class Square : public Shape {
private:
    double side;

public:
    Square(double s) : side(s) {}

    double area() const override {
        return side * side;
    }
};

// 计算图形面积的函数
void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
}

int main() {
    Rectangle rectangle(5, 4);
    Square square(5);

    // 通过基类调用不同的子类,实现多态
    printArea(rectangle);
    printArea(square);

    return 0;
}

在上面的示例中,RectangleSquare 分别是 Shape 的子类,它们都重写了 Shape 中的抽象方法 area()。在 main() 函数中,通过基类 Shape 调用 printArea() 函数,向函数中传递了不同的子类对象,这样就实现了多态。符合里氏替换原则的设计使得我们可以在不修改 printArea() 函数的情况下,很容易地添加新的子类来扩展程序。

开放封闭原则(Open/Closed Principle,OCP)

开放封闭原则由Bertrand Meyer于1988年提出。该原则指出软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。换句话说,一个软件实体应该通过扩展来实现新功能,而不是通过修改现有的代码来实现。

开放封闭原则主要有以下几个要点:

  1. 对扩展开放:意味着当需要添加新的功能时,应该通过扩展现有代码来实现,而不是修改原有代码。新功能应该在不影响现有代码的情况下添加进来。

  2. 对修改关闭:意味着一旦代码编写完成并通过测试,就应该尽量避免修改它。修改现有代码可能会引入新的错误,并且可能会影响到其他部分的代码。

  3. 通过抽象来实现开放封闭原则:可以通过抽象来定义稳定的接口或者基类,而将实现细节延迟到子类或者实现类中。这样可以保持接口的稳定性,同时可以通过子类或者实现类来实现新的功能。

  4. 多态性是实现开放封闭原则的基础:通过多态性,可以在不修改现有代码的情况下,通过子类来扩展和修改程序的行为。这样就实现了对修改关闭,对扩展开放的原则。

以下是一个简单的 C++ 代码示例,演示了开放封闭原则的应用:

cpp 复制代码
#include <iostream>
#include <vector>

// 基类:图形
class Shape {
public:
    virtual double area() const = 0; // 抽象方法:计算面积
};

// 子类:矩形
class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }
};

// 子类:圆形
class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14 * radius * radius;
    }
};

// 计算图形集合的总面积
double totalArea(const std::vector<Shape*>& shapes) {
    double total = 0;
    for (const auto& shape : shapes) {
        total += shape->area();
    }
    return total;
}

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Rectangle(5, 4));
    shapes.push_back(new Circle(3));

    // 计算图形集合的总面积
    std::cout << "Total area: " << totalArea(shapes) << std::endl;

    // 释放内存
    for (const auto& shape : shapes) {
        delete shape;
    }

    return 0;
}

在上面的示例中,RectangleCircle 分别是 Shape 的子类,它们都重写了 Shape 中的抽象方法 area()。通过向 totalArea() 函数中传递一个存储 Shape 指针的向量,我们可以很容易地计算出图形集合的总面积,而不需要修改 totalArea() 函数的代码。这样就实现了对修改关闭,对扩展开放的原则。

迪米特法则(Law of Demeter,LoD)

迪米特法则,又称为最少知识原则(Principle of Least Knowledge),是面向对象设计中的一个重要原则,由迪米特(Demeter)提出。迪米特法则强调一个对象应该对其它对象有尽可能少的了解,也就是说,一个对象不应该直接与太多其他对象发生交互。这样可以降低对象之间的耦合度,提高系统的灵活性和可维护性。

迪米特法则的基本思想可以总结为:

  1. 每个单元(类、模块、函数等)只与其直接的朋友发生交流,不涉及陌生对象。
  2. 如果两个对象之间的交互过于复杂,应该引入中介者对象来解耦。

迪米特法则的核心在于控制对象之间的通信关系,通过限制对象之间的直接交互来降低耦合性。一个对象应该尽可能少地了解其它对象的细节,只与其直接的朋友交流。这样可以避免对象之间的依赖关系过于复杂,使得系统更易于维护和扩展。

迪米特法则的应用可以通过以下几个方面体现:

  1. 合理设计对象的接口:尽量设计简洁清晰的接口,减少对象之间的直接交互。
  2. 使用中介者模式:引入一个中介者对象来管理对象之间的交互,降低对象之间的耦合度。
  3. 封装对象的细节:将对象的内部细节封装起来,只提供必要的接口给外部使用,减少对象之间的依赖关系。
  4. 避免链式调用过多对象的方法:过多的链式调用可能导致对象之间的依赖关系过于复杂,应该尽量避免。

下面是一个简单的 C++ 示例,演示了迪米特法则的应用:

cpp 复制代码
#include <iostream>
#include <vector>

// 学生类
class Student {
public:
    void study() const {
        std::cout << "Student is studying." << std::endl;
    }
};

// 班级类,代表了多个学生的集合
class Class {
private:
    std::vector<Student> students;

public:
    void addStudent(const Student& student) {
        students.push_back(student);
    }

    // 班级负责管理学生的学习行为
    void teach() const {
        for (const auto& student : students) {
            student.study();
        }
    }
};

int main() {
    Student student1;
    Student student2;

    Class class1;
    class1.addStudent(student1);
    class1.addStudent(student2);

    // 班级对象调用学生的学习行为
    class1.teach();

    return 0;
}

在上述代码中,Class 类负责管理学生的学习行为,而不需要直接和 Student 类交互。这符合迪米特法则的原则,即一个对象应该只与其直接的朋友发生交互,不应该和陌生对象直接交互。Class 类只与其直接朋友(即 Student 对象)发生交互,而不会与其他对象直接发生交互,降低了对象之间的耦合度。

合成/聚合复用原则(Composition/Aggregation Reuse Principle,CARP)

合成/聚合复用原则(Composition/Aggregation Reuse Principle,CARP)是面向对象设计中的一个重要原则,它强调应该优先使用对象组合和聚合,而不是通过继承来达到代码复用的目的。该原则的核心思想是在新的对象中使用已有的对象,而不是通过继承来获得已有对象的行为。

合成/聚合复用原则的主要思想包括:

  1. 优先使用对象组合和聚合:在设计类时,应该优先考虑将已有的类对象作为成员变量组合到新的类中,或者通过聚合关系将已有的类对象组合起来,而不是通过继承来获得已有类的行为。

  2. 对象之间的关系更灵活:通过对象组合和聚合可以实现更灵活的对象之间的关系,对象的行为不会受到父类的限制,避免了类之间过于紧密的耦合。

  3. 减少继承带来的副作用:通过对象组合和聚合可以避免继承带来的副作用,例如子类与父类之间的强耦合、父类的修改可能影响到子类等问题。

  4. 增强代码的可维护性和可扩展性:通过对象组合和聚合可以降低类之间的耦合度,提高代码的可维护性和可扩展性,使得系统更加灵活和易于理解。

以下是一个简单的 C++ 代码示例,演示了合成/聚合复用原则的应用:

cpp 复制代码
#include <iostream>
#include <string>

// 飞行行为接口
class FlyBehavior {
public:
    virtual void fly() const = 0;
};

// 实现了飞行行为的类
class FlyWithWings : public FlyBehavior {
public:
    void fly() const override {
        std::cout << "Flying with wings!" << std::endl;
    }
};

// 实现了不会飞行的类
class FlyNoWay : public FlyBehavior {
public:
    void fly() const override {
        std::cout << "Unable to fly!" << std::endl;
    }
};

// 鸭子类
class Duck {
protected:
    FlyBehavior* flyBehavior; // 使用对象组合

public:
    Duck(FlyBehavior* fb) : flyBehavior(fb) {}

    virtual void performFly() const {
        flyBehavior->fly();
    }

    virtual void display() const = 0;
};

// 绿头鸭类
class MallardDuck : public Duck {
public:
    MallardDuck() : Duck(new FlyWithWings()) {} // 使用对象组合

    void display() const override {
        std::cout << "Displaying Mallard Duck" << std::endl;
    }
};

int main() {
    MallardDuck duck;
    duck.display();
    duck.performFly();

    return 0;
}

上述代码中,FlyBehavior 是一个飞行行为的接口,而 FlyWithWingsFlyNoWay 分别是实现了飞行行为的具体类。Duck 类通过对象组合的方式持有一个 FlyBehavior 对象,而不是通过继承来获得飞行行为。这样做符合合成/聚合复用原则,使得 Duck 类可以根据需要灵活地组合不同的飞行行为,而不会受到继承带来的限制。

相关推荐
芊寻(嵌入式)9 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
WaaTong10 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
獨枭10 分钟前
C++ 项目中使用 .dll 和 .def 文件的操作指南
c++
霁月风13 分钟前
设计模式——观察者模式
c++·观察者模式·设计模式
橘色的喵14 分钟前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier
一颗松鼠17 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
有梦想的咸鱼_19 分钟前
go实现并发安全hashtable 拉链法
开发语言·golang·哈希算法
海阔天空_201324 分钟前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化
天下皆白_唯我独黑32 分钟前
php 使用qrcode制作二维码图片
开发语言·php
夜雨翦春韭35 分钟前
Java中的动态代理
java·开发语言·aop·动态代理