【设计模式】SOLID设计原则

1、什么是SOLID设计原则

SOLID 是面向对象设计中的五个基本设计原则的首字母缩写,它们是:

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

类应该只有一个单一的职责,即一个类应该有且只有一个改变的理由。这意味着一个类应该只负责一个特定的功能或任务,而不是多个不相关的功能。这样做可以提高类的内聚性,并使得类更容易理解、修改和测试。

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

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着在不修改现有代码的情况下,应该能够通过添加新的代码来扩展系统的功能。这样做可以使得系统更加稳定,减少修改现有代码可能带来的风险。

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

子类型必须能够替换其基类型。换句话说,任何可以接受基类型的地方都可以接受子类型,而且不会引发意外的行为。这样做可以保持系统的一致性和可靠性,并且确保使用继承时不会破坏代码的正确性。

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

客户端不应该被迫依赖于其不使用的接口。这意味着应该将接口设计成小而专注的接口,而不是大而臃肿的接口。这样做可以降低耦合性,并且使得系统更加灵活和易于维护。

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

高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。这样做可以降低模块之间的耦合度,并且使得系统更易于扩展和修改。

这些原则是由罗伯特·马丁(Robert C. Martin)等人在面向对象设计中提出的,它们提供了一套指导原则,帮助设计出高质量、可维护和可扩展的面向对象系统。

2、单一职责原则

单一职责原则(Single Responsibility Principle,SRP)要求一个类或模块应该只有一个单一的责任,即一个类或模块应该只负责一个特定的功能或任务。这样做可以提高代码的内聚性、可维护性和可测试性。

让我们通过一个简单的例子来说明单一职责原则:

假设我们有一个简单的应用程序,用于处理用户信息,包括保存用户信息到数据库和从数据库中检索用户信息。我们可以将这个功能拆分成两个类:一个负责保存用户信息,一个负责检索用户信息。

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

// 负责保存用户信息到数据库
class UserSaver {
public:
    void saveUser(const std::string& username, const std::string& email) {
        // 将用户信息保存到数据库
        std::cout << "用户信息已保存到数据库:" << username << ", " << email << std::endl;
    }
};

// 负责从数据库中检索用户信息
class UserRetriever {
public:
    void retrieveUser(const std::string& username) {
        // 从数据库中检索用户信息
        std::cout << "从数据库中检索到用户信息:" << username << std::endl;
    }
};

int main() {
    UserSaver userSaver;
    userSaver.saveUser("Alice", "alice@example.com");

    UserRetriever userRetriever;
    userRetriever.retrieveUser("Alice");

    return 0;
}

在这个例子中,我们有两个类 UserSaver 和 UserRetriever,它们分别负责保存用户信息和检索用户信息。这两个类各自都只有一个单一的职责,即负责一个特定的功能。如果我们需要修改保存用户信息的逻辑,我们只需要修改 UserSaver 类;如果我们需要修改检索用户信息的逻辑,我们只需要修改 UserRetriever 类。这样做提高了代码的可维护性,并且使得每个类更加简单和易于理解。

3、开放-封闭原则

开放-封闭原则(Open/Closed Principle,OCP)是面向对象设计中的一个基本原则,由柏拉图·梅特克斯(Bertrand Meyer)在他的《面向对象软件构造》(Object-Oriented Software Construction)一书中首次提出。它的核心思想是软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。换句话说,软件实体在不修改现有代码的情况下,应该能够通过添加新的代码来扩展系统的功能。

开放-封闭原则的目的是为了提高系统的可维护性、可扩展性和稳定性。通过遵循这一原则,可以使得系统更容易理解和修改,并且减少对现有代码的影响。

在实际应用中,可以通过以下几种方式来遵循开放-封闭原则:

抽象化:通过使用抽象类、接口或者抽象函数来定义可扩展的接口,从而使得系统可以根据需要进行扩展,而不必修改现有代码。

多态性:利用多态性和继承机制,使得系统可以通过添加新的子类来扩展功能,而不必修改基类或现有代码。

组合/聚合:通过组合或聚合关系来构建对象之间的关联关系,从而使得系统可以通过添加新的组件来扩展功能,而不必修改现有组件。

模块化:将系统分解成独立的模块或组件,使得每个模块只负责一个特定的功能,从而使得系统可以通过添加新的模块来扩展功能,而不必修改现有模块。

总之,开放-封闭原则指导我们设计出易于扩展和维护的软件系统,通过封装变化和利用多态性,使得系统可以根据需要进行扩展,而不必修改现有代码。

让我们通过一个简单的例子来说明开放-封闭原则。

假设我们有一个简单的图形绘制程序,它可以绘制不同形状的图形,包括圆形和矩形。现在我们希望在程序中添加新的图形类型,比如三角形。我们可以通过遵循开放-封闭原则来扩展程序的功能,而不必修改现有的代码。

首先,我们定义一个抽象基类 Shape,它有一个纯虚函数 draw 用于绘制图形:

cpp 复制代码
#include <iostream>

// 抽象基类:图形
class Shape {
public:
    virtual void draw() const = 0;
};

然后,我们定义具体的图形类,比如 Circle 和 Rectangle 类,它们分别继承自 Shape 类并实现 draw 函数:

cpp 复制代码
// 圆形类
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "绘制圆形\n";
    }
};

// 矩形类
class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "绘制矩形\n";
    }
};

现在,如果我们想要添加新的图形类型,比如三角形,我们只需要添加一个新的类 Triangle,它也继承自 Shape 类并实现 draw 函数:

cpp 复制代码
// 三角形类
class Triangle : public Shape {
public:
    void draw() const override {
        std::cout << "绘制三角形\n";
    }
};

通过这种方式,我们可以在不修改现有代码的情况下,通过添加新的类来扩展程序的功能,符合开放-封闭原则。这样做提高了代码的可维护性和可扩展性,使得系统更易于理解和修改。

4、 里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计中的一个基本原则,由芭芭拉·利斯科夫(Barbara Liskov)在 1987 年提出。该原则指出,子类型必须能够替换其基类型,即任何可以接受基类型的地方都可以接受子类型,而且不会引发意外的行为。

在更通俗的说法中,如果一个类型是子类型(派生类),那么它应该可以替换掉基类型(基类)并且不会破坏程序的正确性。换句话说,子类型应该保持基类型的行为,而不是产生意外的行为。

遵循里氏替换原则的目的是为了确保代码的一致性和可靠性,使得系统更易于理解、扩展和维护。如果违反了里氏替换原则,那么可能会导致程序的错误行为和不稳定性。

在实际应用中,可以通过以下几点来遵循里氏替换原则:

子类型必须实现基类型的所有行为,不能减少基类型的约束条件。

子类型可以增加新的行为,但不能修改基类型已有的行为。

子类型的前置条件(即输入条件)必须比基类型更宽松。

子类型的后置条件(即输出条件)必须比基类型更严格。

通过遵循里氏替换原则,可以确保系统的稳定性和可靠性,并且使得系统更易于扩展和维护。

让我们通过一个简单的例子来说明里氏替换原则。

假设我们有一个简单的几何图形类层次结构,包括基类 Shape 和两个子类 Rectangle 和 Square,其中 Square 是 Rectangle 的子类。

现在让我们来看看是否满足里氏替换原则:

cpp 复制代码
#include <iostream>

// 基类:图形
class Shape {
public:
    virtual void draw() const {
        std::cout << "绘制图形\n";
    }
};

// 矩形类
class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "绘制矩形\n";
    }
};

// 正方形类
class Square : public Rectangle {
public:
    void draw() const override {
        std::cout << "绘制正方形\n";
    }
};

// 绘制图形函数
void drawShape(const Shape& shape) {
    shape.draw();
}

int main() {
    Rectangle rectangle;
    Square square;

    drawShape(rectangle); // 绘制矩形
    drawShape(square);    // 绘制正方形

    return 0;
}

在这个例子中,我们有一个基类 Shape,它有一个 draw 方法用于绘制图形。然后,我们有一个 Rectangle 类和一个 Square 类,它们分别继承自 Shape 类,并且都重写了 draw 方法以实现各自特定的绘制行为。

在 main 函数中,我们创建了一个 Rectangle 对象和一个 Square 对象,并且分别调用了 drawShape 函数来绘制这些图形。

在这个例子中,Square 类是 Rectangle 类的子类,符合继承关系。而且,在 drawShape 函数中,我们可以接受 Shape 类型的参数,并且传入 Rectangle 或 Square 对象进行绘制,而不会产生意外的行为。

因此,这个例子满足了里氏替换原则:子类型(Square)可以替换其基类型(Rectangle)而不会引发意外的行为,程序的行为保持一致。

5、接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计中的一个基本原则,由罗伯特·马丁(Robert C. Martin)在他的《敏捷软件开发:原则、模式和实践》(Agile Software Development, Principles, Patterns, and Practices)一书中提出。接口隔离原则指出,客户端不应该被迫依赖于其不使用的接口。换句话说,一个类不应该依赖于它不需要使用的接口,应该将接口设计成小而专注的接口,而不是大而臃肿的接口。

接口隔离原则的目的是为了提高系统的灵活性和可维护性。通过将接口拆分成小而专注的接口,可以降低类之间的耦合度,使得系统更易于理解、扩展和修改。同时,这也可以避免因为接口的臃肿而导致的功能耦合和代码冗余。

在实践中,可以通过以下几点来遵循接口隔离原则:

  1. 将大而臃肿的接口拆分成多个小而专注的接口,每个接口只包含一个单一的功能或职责。
  2. 只在需要使用某个接口的地方引入该接口,避免将不需要的接口强加给客户端。
  3. 根据客户端的需求,设计出合适的接口,并且保持接口的稳定性,避免频繁地修改接口。

通过遵循接口隔离原则,可以使得系统更灵活、更易于维护,并且能够更好地应对需求变化。

让我们通过一个简单的例子来说明接口隔离原则。

假设我们有一个简单的文件操作接口 FileOperation,它定义了一些文件操作的方法,比如打开文件、读取文件和关闭文件等。然后,我们有两个类 TextEditor 和 ImageEditor,它们分别实现了这个接口。

首先,让我们定义文件操作接口 FileOperation:

cpp 复制代码
#include <iostream>

// 文件操作接口
class FileOperation {
public:
    virtual void open() = 0;
    virtual void read() = 0;
    virtual void close() = 0;
};

然后,我们有一个文本编辑器 TextEditor,它需要实现文件操作接口来打开和读取文本文件:

cpp 复制代码
// 文本编辑器类
class TextEditor : public FileOperation {
public:
    void open() override {
        std::cout << "打开文本文件\n";
    }

    void read() override {
        std::cout << "读取文本文件\n";
    }

    void close() override {
        std::cout << "关闭文本文件\n";
    }
};

接着,我们有一个图像编辑器 ImageEditor,它也需要实现文件操作接口来打开和读取图像文件:

cpp 复制代码
// 图像编辑器类
class ImageEditor : public FileOperation {
public:
    void open() override {
        std::cout << "打开图像文件\n";
    }

    void read() override {
        std::cout << "读取图像文件\n";
    }

    void close() override {
        std::cout << "关闭图像文件\n";
    }
};

在这个例子中,TextEditor 和 ImageEditor 都实现了 FileOperation 接口,但是它们只使用了其中的一部分方法(即打开和读取文件)。如果我们将所有文件操作都放在一个大的接口中,那么 TextEditor 和 ImageEditor 就不得不实现它们不需要的方法,违反了接口隔离原则。

通过将接口设计成小而专注的接口,每个接口只包含一个单一的功能或职责,我们遵循了接口隔离原则,并且使得系统更易于理解、扩展和修改。

6、依赖倒置原则

依赖倒置原则(Dependency Inversion Principle,DIP)是面向对象设计中的一个基本原则,由罗伯特·马丁(Robert C. Martin)在他的《敏捷软件开发:原则、模式和实践》(Agile Software Development, Principles, Patterns, and Practices)一书中提出。依赖倒置原则指出,高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

依赖倒置原则的核心思想是通过使用抽象来降低类之间的耦合度,从而使得系统更加灵活、可扩展和易于维护。具体来说,依赖倒置原则要求我们将程序的设计重心放在抽象上,而不是具体实现上,通过使用接口、抽象类或者依赖注入等方式来实现依赖倒置。

在实践中,可以通过以下几点来遵循依赖倒置原则:

  1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。即高层模块和低层模块都应该依赖于同一个抽象接口或抽象类。
  2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。即抽象接口或抽象类不应该依赖于具体实现,而是具体实现应该依赖于抽象接口或抽象类。
  3. 可以通过依赖注入(Dependency Injection)等方式来实现依赖倒置,将具体实现的创建和注入交给外部,而不是在类内部创建具体实现的对象。

通过遵循依赖倒置原则,可以使得系统更加灵活、可扩展和易于维护,减少类之间的耦合度,提高代码的可复用性和可测试性。

让我们通过一个简单的例子来说明依赖倒置原则。

假设我们有一个简单的电子邮件发送系统,其中包含一个 EmailSender 类用于发送电子邮件。一开始,EmailSender 类直接依赖于具体的邮件服务提供商,比如 Gmail。这个设计违反了依赖倒置原则,因为高层模块 EmailSender 直接依赖于低层模块,即具体的邮件服务提供商。

cpp 复制代码
#include <iostream>

// 具体的邮件服务提供商:Gmail
class Gmail {
public:
    void sendEmail(const std::string& recipient, const std::string& message) {
        std::cout << "Sending email to " << recipient << " via Gmail: " << message << std::endl;
    }
};

// 邮件发送类
class EmailSender {
private:
    Gmail gmail;

public:
    void sendEmail(const std::string& recipient, const std::string& message) {
        gmail.sendEmail(recipient, message);
    }
};

int main() {
    EmailSender sender;
    sender.sendEmail("example@example.com", "Hello, this is a test email.");

    return 0;
}

现在,让我们通过引入抽象来遵循依赖倒置原则。我们可以定义一个抽象的邮件服务接口 EmailService,并让 Gmail 类实现这个接口。然后,EmailSender 类只依赖于 EmailService 接口,而不是具体的邮件服务提供商。

cpp 复制代码
#include <iostream>

// 抽象的邮件服务接口
class EmailService {
public:
    virtual void sendEmail(const std::string& recipient, const std::string& message) = 0;
};

// 具体的邮件服务提供商:Gmail
class Gmail : public EmailService {
public:
    void sendEmail(const std::string& recipient, const std::string& message) override {
        std::cout << "Sending email to " << recipient << " via Gmail: " << message << std::endl;
    }
};

// 邮件发送类
class EmailSender {
private:
    EmailService* emailService;

public:
    EmailSender(EmailService* service) : emailService(service) {}

    void sendEmail(const std::string& recipient, const std::string& message) {
        emailService->sendEmail(recipient, message);
    }
};

int main() {
    Gmail gmail;
    EmailSender sender(&gmail);
    sender.sendEmail("example@example.com", "Hello, this is a test email.");

    return 0;
}

通过这种方式,EmailSender 类不再直接依赖于具体的邮件服务提供商,而是依赖于抽象的邮件服务接口 EmailService。这样做符合依赖倒置原则,使得系统更加灵活、可扩展和易于维护,因为现在可以轻松地切换不同的邮件服务提供商,而不需要修改 EmailSender 类的代码。

相关推荐
倒头就睡的小比特5 天前
算法竞赛C++常用的STL
c++·算法
weilx12345 天前
C++笔记-文件IO-<fcntl.h>
c++
小羊没烦恼!5 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
伞伞悦读5 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
Smileyqp沛沛5 天前
前端?C++ ?较大差异基础罗列
c++·基础·前端转c++
C语言小火车5 天前
C/C++ 为什么需要编译器?
开发语言·c++
旖旎夜光5 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
吞下星星的少年·-·5 天前
C++ 萌新语法入门篇
c++·算法比赛
霍霍的袁5 天前
【C++】map 和 set 的使用 | 从用法到底层
开发语言·c++·学习·visual studio
another heaven5 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法