常见设计原则

常用设计原则目录

单一职责原则

  • 单一职责原则(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 集合,符合迪米特法则。
相关推荐
博睿谷IT99_12 小时前
MySQL OCP 认证限时免费活动 7 月 31 日 前截止!!!
数据库·mysql·开闭原则
博睿谷IT99_12 小时前
MySQL OCP和Oracle OCP怎么选?
数据库·mysql·oracle·开闭原则·ocp
进击的CJR16 小时前
MySQL 8.0 OCP 英文题库解析(二)
android·mysql·开闭原则
IT专业服务商2 天前
DELL R770 服务器,更换OCP模块!
运维·服务器·硬件架构·硬件工程·开闭原则
常某某的好奇心3 天前
开闭原则(OCP)
开闭原则
一只栖枝5 天前
Oracle OCP证书有效期是三年?
数据库·oracle·开闭原则·ocp证书·考试流程
常某某的好奇心5 天前
里氏替换原则(LSP)
里氏替换原则
常某某的好奇心6 天前
开闭原则与依赖倒置原则区别:原类不变,新增类(功能)vs 接口类不变,原实现类可变
依赖倒置原则·开闭原则
小白考证进阶中12 天前
0基础可以考MySQL OCP么?备考时间需要多久?
数据库·mysql·开闭原则
一只栖枝13 天前
关于OCP认证:有Oracle和MySQL两种
数据库·mysql·oracle·开闭原则·数据管理·ocp认证