常见设计原则

常用设计原则目录

单一职责原则

  • 单一职责原则(Single Responsibility Principle, SRP)
  • 定义:
    一个类应该只有一个引起它变化的原因。即一个类应该只负责一项职责。
  • 解释:
    这意味着一个类应该专注于一个功能或一个业务逻辑。如果一个类承担了多个不同的职责,那么对其中一个职责的修改可能会影响到其他职责,导致难以维护和测试。例如,一个类既负责用户数据的存储又负责用户数据的验证,当存储逻辑需要修改时,可能会影响到验证逻辑,反之亦然。
    例如,在一个电商系统中, OrderService 类应该只负责订单的处理,如创建订单、修改订单、取消订单等操作,而不应该同时负责处理用户的支付和商品的库存管理。
cpp 复制代码
#include <iostream>
#include <string>

// 用户验证类,只负责用户验证功能
class UserValidator {
public:
    bool validate(const std::string& username, const std::string& password) {
        // 简单的验证逻辑,这里仅作示例
        if (username == "admin" && password == "123456") {
            return true;
        }
        return false;
    }
};

// 用户存储类,只负责用户存储功能
class UserStorage {
public:
    void save(const std::string& username, const std::string& password) {
        // 简单的存储逻辑,这里仅作示例
        std::cout << "Saving user: " << username << " with password: " << password << std::endl;
    }
};

int main() {
    UserValidator validator;
    UserStorage storage;

    std::string username = "admin";
    std::string password = "123456";

    if (validator.validate(username, password)) {
        storage.save(username, password);
    } else {
        std::cout << "Invalid user" << std::endl;
    }
    return 0;
}

注释:

  • UserValidator 类只负责用户验证,通过 validate 方法验证用户名和密码。
  • UserStorage 类只负责用户存储,通过 save 方法存储用户信息。这样,每个类只负责一项职责,符合单一职责原则。

开闭原则

  • 开闭原则(Open/Closed Principle, OCP)
  • 定义:
    软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
  • 解释:
    当需要对系统添加新功能时,应该通过扩展现有代码(例如通过继承、实现接口等方式)而不是修改现有代码来实现。这样可以确保现有代码的稳定性,避免因修改旧代码而引入新的错误。
    例如,在一个图形绘制系统中,有一个 Shape 基类,其派生类有 Circle、Rectangle 等。如果要添加新的形状 Triangle,应该创建一个新的 Triangle 类继承自 Shape,而不是修改 Shape 类的代码。这样,Shape 类及其已有的派生类的代码保持不变,实现了对修改关闭,对扩展开放。
cpp 复制代码
#include <iostream>
#include <memory>
#include <vector>

// 基类 Shape,定义一个绘制形状的抽象接口
class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};

// 具体的圆形类,实现 Shape 接口
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

// 具体的矩形类,实现 Shape 接口
class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

// 图形绘制器类,负责绘制各种形状
class ShapeDrawer {
public:
    void drawShapes(const std::vector<std::shared_ptr<Shape>>& shapes) {
        for (const auto& shape : shapes) {
            shape->draw();
        }
    }
};

int main() {
    std::vector<std::shared_ptr<Shape>> shapes;
    shapes.push_back(std::make_shared<Circle>());
    shapes.push_back(std::make_shared<Rectangle>());

    ShapeDrawer drawer;
    drawer.drawShapes(shapes);

    // 新增一个三角形类,扩展功能而不修改现有代码
    class Triangle : public Shape {
    public:
        void draw() override {
            std::cout << "Drawing a triangle" << std::endl;
        }
    };

    shapes.push_back(std::make_shared<Triangle>());
    drawer.drawShapes(shapes);

    return 0;
}

注释:

  • Shape 是一个抽象基类,定义了 draw 接口,CircleRectangle 是其具体实现。
  • ShapeDrawer 类的 drawShapes 方法可以绘制各种形状,通过使用 Shape
    的指针,它可以绘制不同的形状而不关心具体的形状类型。
  • 新增 Triangle 类时,无需修改 ShapeDrawer 类的代码,符合开闭原则。

里氏替换原则

  • 里氏替换原则(Liskov Substitution Principle, LSP)
  • 定义:
    子类型必须能够替换掉它们的父类型,而程序的行为不会发生变化。
  • 解释:
    这意味着派生类应该可以在任何使用基类的地方被使用,并且不会破坏程序的正确性。派生类在重写基类的方法时,不能改变基类方法的前置条件和后置条件,不能改变基类方法的输入输出约定。
    例如,如果 Bird 是一个基类,PenguinBird 的一个派生类,那么在使用 Bird 的地方,使用 Penguin 替换时,不应该出现错误。如果 Bird 类有一个fly 方法,而 Penguin 不能飞,那么在 Penguin 类中不应该简单地重写 fly 方法为抛出异常或不做任何事情,而应该重新考虑类的继承关系或方法的设计,因为这违反了里氏替换原则。
cpp 复制代码
#include <iostream>

// 基类 Bird,定义一个飞行接口
class Bird {
public:
    virtual void fly() = 0;
    virtual ~Bird() = default;
};

// 能正常飞行的鸟
class Sparrow : public Bird {
public:
    void fly() override {
        std::cout << "Sparrow is flying" << std::endl;
    }
};

// 不会飞行的鸟,重新设计接口以符合 LSP
class Penguin {
public:
    void swim() {
        std::cout << "Penguin is swimming" << std::endl;
    }
};

// 鸟的操作类,使用 Bird 基类
class BirdOperation {
public:
    void makeBirdFly(Bird* bird) {
        bird->fly();
    }
};

int main() {
    Sparrow sparrow;
    BirdOperation operation;
    operation.makeBirdFly(&sparrow);

    // 企鹅类不继承自 Bird,避免违反 LSP
    Penguin penguin;
    // 不会调用企鹅的 fly 方法,因为企鹅不应该有 fly 接口
    // 如果企鹅继承自 Bird 并抛出异常或不做任何事情,就违反了 LSP
    return 0;
}

注释:

  • Bird 是基类,定义了 fly 接口,SparrowBird 的子类,实现了正常飞行。
  • Penguin 类不继承自 Bird,因为它不能飞行。如果让 Penguin 继承 Bird 并实现 fly
    接口(如抛出异常),就会违反里氏替换原则,这里通过重新设计类结构避免了这个问题。
  • BirdOperation 类使用 Bird 基类,调用 fly 方法。

接口隔离原则

  • 接口隔离原则(Interface Segregation Principle, ISP)
  • 定义:
    客户端不应该依赖它不需要的接口。
  • 解释:
    应该将大而全的接口拆分成多个小的、更具体的接口,使客户端只依赖于它们真正需要的接口,避免强迫客户端实现它们不使用的方法。
    例如,有一个 Worker 接口,包含 workeatsleep 方法。对于一个 RobotWorker 类,它不需要 eatsleep 方法,所以可以将 Worker 接口拆分成 Workable(包含 work)、Eatable(包含 eat)和 Sleepable(包含 sleep)接口,这样 RobotWorker 只需要实现 Workable 接口,而不用实现 Eatable Sleepable 接口。
cpp 复制代码
#include <iostream>

// 可工作的接口
class Workable {
public:
    virtual void work() = 0;
    virtual ~Workable() = default;
};

// 可吃的接口
class Eatable {
public:
    virtual void eat() = 0;
    virtual ~Eatable() = default;
};

// 可睡的接口
class Sleepable {
public:
    virtual void sleep() = 0;
    virtual ~Sleepable() = default;
};

// 人类,实现了 Workable, Eatable, Sleepable 接口
class Human : public Workable, public Eatable, public Sleepable {
public:
    void work() override {
        std::cout << "Human is working" << std::endl;
    }
    void eat() override {
        std::cout << "Human is eating" << std::endl;
    }
    void sleep() override {
        std::cout << "Human is sleeping" << std::endl;
    }
};

// 机器人,只实现 Workable 接口
class Robot : public Workable {
public:
    void work() override {
        std::cout << "Robot is working" << std::endl;
    }
};

int main() {
    Human human;
    human.work();
    human.eat();
    human.sleep();

    Robot robot;
    robot.work();
    // 机器人不需要 eat 和 sleep 功能,避免实现不必要的接口
    return 0;
}

注释:

  • Workable、Eatable 和 Sleepable 是不同的接口,将不同的行为分离。
  • Human 实现了三个接口,因为人类可以工作、吃和睡。
  • Robot 只实现 Workable 接口,因为机器人不需要吃和睡,符合接口隔离原则。

依赖倒置原则

  • 依赖倒置原则(Dependency Inversion Principle, DIP)
  • 定义:
    高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
  • 解释:
    这意味着在软件设计中,应该依赖于抽象接口或抽象类,而不是具体的实现类。通过依赖抽象,可以降低模块之间的耦合度,提高代码的可维护性和可扩展性。
    例如,在一个汽车制造系统中,Car 类(高层模块)不应该直接依赖于具体的 Engine 实现类,而是依赖于一个抽象的 IEngine 接口。不同的 Engine 实现(如 GasolineEngine、ElectricEngine)可以实现 IEngine 接口,Car 类可以使用不同的 IEngine 实现,这样当需要更换引擎时,只需要替换 IEngine 的具体实现,而不需要修改 Car 类。
cpp 复制代码
#include <iostream>

// 抽象引擎接口
class IEngine {
public:
    virtual void start() = 0;
    virtual ~IEngine() = default;
};

// 汽油引擎实现
class GasolineEngine : public IEngine {
public:
    void start() override {
        std::cout << "Gasoline engine starts" << std::endl;
    }
};

// 电动引擎实现
class ElectricEngine : public IEngine {
public:
    void start() override {
        std::cout << "Electric engine starts" << std::endl;
    }
};

// 汽车类,依赖于抽象引擎接口
class Car {
private:
    IEngine* engine;
public:
    Car(IEngine* e) : engine(e) {}
    void start() {
        engine->start();
    }
};

int main() {
    // 使用汽油引擎
    IEngine* gasolineEngine = new GasolineEngine();
    Car car1(gasolineEngine);
    car1.start();

    // 使用电动引擎
    IEngine* electricEngine = new ElectricEngine();
    Car car2(electricEngine);
    car2.start();

    delete gasolineEngine;
    delete electricEngine;
    return 0;
}

注释:

  • IEngine 是抽象引擎接口,GasolineEngineElectricEngine 是具体实现。
  • Car 类依赖于 IEngine 接口,而不是具体的引擎实现。这样可以通过传递不同的引擎实现类给 Car 类,实现了依赖倒置原则。

迪米特法则

  • 迪米特法则(Law of Demeter, LoD),也称为最少知识原则(Least Knowledge Principle)
  • 定义:
    一个对象应该对其他对象有最少的了解,只与直接朋友通信,避免耦合度过高。
  • 解释:
    一个对象的 "朋友" 包括:当前对象本身、作为参数传递进来的对象、当前对象的成员变量、如果成员变量是一个集合,那么集合中的元素、当前对象创建的对象。该原则要求对象之间的通信应该尽可能地少,避免一个对象对另一个对象内部的细节有过多的了解。
    例如,在一个公司系统中,Employee 类不应该直接调用 Department 类的 Manager 类的方法,而是通过 Department 类提供的接口来间接调用,这样 Employee 类只需要和 Department 类通信,而不用深入到 Department类内部的 Manager 类。
cpp 复制代码
#include <iostream>
#include <vector>

class Employee {
private:
    std::string name;
public:
    Employee(const std::string& n) : name(n) {}
    std::string getName() const {
        return name;
    }
};

class Department {
private:
    std::vector<Employee> employees;
public:
    void addEmployee(const Employee& e) {
        employees.push_back(e);
    }
    std::vector<Employee> getEmployees() const {
        return employees;
    }
};

class Company {
private:
    std::vector<Department> departments;
public:
    void addDepartment(const Department& d) {
        departments.push_back(d);
    }
    void printEmployees() {
        for (const auto& department : departments) {
            // 通过 Department 提供的接口获取员工信息,而不是直接访问 Department 内部的 Employee 集合
            for (const auto& employee : department.getEmployees()) {
                std::cout << employee.getName() << std::endl;
            }
        }
    }
};

int main() {
    Company company;
    Department department;
    department.addEmployee(Employee("Alice"));
    department.addEmployee(Employee("Bob"));
    company.addDepartment(department);
    company.printEmployees();
    return 0;
}

注释:

  • Employee 类表示员工,包含员工的基本信息。
  • Department 类管理员工,包含添加和获取员工的方法。
  • Company 类管理部门,通过 DepartmentgetEmployees 接口获取员工信息,而不是直接操作
    Department 类中的 employees 集合,符合迪米特法则。
相关推荐
萤火虫Coding2 天前
SOLID-开闭原则
java·设计模式·设计原则·开闭原则·solid·open-closed
若亦_Royi4 天前
C++通透讲解设计模式:开闭原则(1)
c++·设计模式·开闭原则
Danileaf_Guo7 天前
MPLS小实验:利用LDP动态建立LSP
里氏替换原则
向宇it11 天前
【从零开始入门unity游戏开发之——C#篇26】C#面向对象动态多态——接口(Interface)、接口里氏替换原则、密封方法(`sealed` )
java·开发语言·unity·c#·游戏引擎·里氏替换原则
向宇it13 天前
【从零开始入门unity游戏开发之——C#篇23】C#面向对象继承——`as`类型转化和`is`类型检查、向上转型和向下转型、里氏替换原则(LSP)
java·开发语言·unity·c#·游戏引擎·里氏替换原则
Danileaf_Guo14 天前
MPLS小实验:静态建立LSP
网络·里氏替换原则
比格丽巴格丽抱15 天前
spring\strust\springboot\isp前后端那些事儿
spring boot·spring·接口隔离原则
爱跨境的笑笑22 天前
ISP 代理提供商:互联网安全的关键参与者
网络·网络协议·tcp/ip·智能路由器·ip·接口隔离原则
若亦_Royi1 个月前
C++通透讲解设计模式:单一职责原则
c++·设计模式·单一职责原则