当前位置: 首页 > article >正文

[C++设计模式] 深入理解面向对象设计原则


文章目录

  • 面向对象设计:为什么?
      • 变化是复用的天敌
      • 面向对象的价值:抵御变化
  • 重新认识面向对象
      • 理解“隔离变化”
      • 各司其职
      • 对象是什么?
  • 面向对象设计原则
      • 单一职责原则 (SRP)
      • 开放封闭原则 (OCP)
      • **里氏替换原则 (LSP)**
      • **依赖倒置原则 (DIP)**
      • **接口隔离原则 (ISP)**
      • **优先使用对象组合,而不是类继承**
      • **封装变化点**
      • **针对接口编程,而不是针对实现编程**
      • 总结
      • 四、面向接口设计
        • 示例:标准化接口
      • 五、将设计原则提升为设计经验
      • 总结

在了解为什么需要设计模式之后,我们再来深入理解关于面向对象的设计原则。

在软件开发中,变化是永恒的主题。当需求发生变化时,设计的灵活性和可扩展性便成为系统能否持续演进的关键。设计模式的核心就在于利用面向对象设计原则,帮助开发者构建既能应对变化又具有高度复用性的系统。

本文将从面向对象设计的必要性入手,逐步分析其设计原则和应用,并探讨如何将这些原则上升为设计经验。


面向对象设计:为什么?

变化是复用的天敌

在软件开发中,“变化”无处不在:

  • 用户需求不断更新。
  • 新技术不断涌现。
  • 系统规模不断扩大。

复用性灵活性往往因变化而受到挑战。面向对象设计的目标在于通过一系列原则和模式,隔离变化点,让系统的核心逻辑在变化中保持稳定。

面向对象的价值:抵御变化

面向对象设计通过封装和抽象,将变化控制在可预见的范围内:

  • 封装变化点:将容易变化的部分封装起来,不影响其他模块。
  • 抽象接口:通过接口定义不变的部分,隐藏具体实现的变化。

重新认识面向对象

理解“隔离变化”

面向对象的核心思想是隔离变化:让容易变化的部分独立,不干扰稳定的部分。

宏观角度来看,面向对象的构建方式更能适应软件的变化,能将变化所带来的影响减为最小。

各司其职

微观角度来看,每个类、对象都应该承担明确的职责。职责越单一,变化的影响范围越小,系统的可维护性越高。由于需求所导致的新增类型不应该影响原来类型的实现,即为各司其职

对象是什么?

**语言层面:**对象封装了代码和数据。

**规格层面:**对象是一系列可以被直接使用的公共接口。

**概念层面:**对象是某种拥有责任 (功能) 的抽象。

对象是数据和行为的封装体。它不仅包含数据,还定义了操作这些数据的方法。对象的使命在于协同完成任务,同时隐藏实现细节。


面向对象设计原则

核心:高内聚,低耦合。

单一职责原则 (SRP)

定义:一个类只应该有一个引起其变化的原因。即,每个类的职责应当单一。
目的:降低类的复杂性,减少因为职责过多而导致的变更影响。

示例代码:一个打印报表的系统。

#include <iostream>
#include <string>
using namespace std;

// 负责生成报表数据
class ReportGenerator {
public:
    string generateReport() {
        return "Report Content";
    }
};

// 负责打印报表
class ReportPrinter {
public:
    void printReport(const string& reportContent) {
        cout << "Printing Report: " << reportContent << endl;
    }
};

int main() {
    ReportGenerator generator;
    ReportPrinter printer;

    // 生成并打印报表
    string report = generator.generateReport();
    printer.printReport(report);

    return 0;
}

为何实现SRP

  • ReportGenerator 只负责生成报表,不负责打印。
  • ReportPrinter 只负责打印报表,不关心生成逻辑。
  • 任何一方变化(如改变打印逻辑)都不会影响另一方。

开放封闭原则 (OCP)

定义:软件实体(类、模块、函数)应该对扩展开放,对修改封闭。
目的:通过扩展实现新功能,而不是通过修改现有代码。

示例代码:不同类型图形的绘制。

#include <iostream>
#include <vector>
using namespace std;

// 图形基类
class Shape {
public:
    virtual void draw() const = 0; // 抽象方法:绘制
    virtual ~Shape() = default;
};

// 圆形实现
class Circle : public Shape {
public:
    void draw() const override {
        cout << "Drawing Circle" << endl;
    }
};

// 矩形实现
class Rectangle : public Shape {
public:
    void draw() const override {
        cout << "Drawing Rectangle" << endl;
    }
};

// 渲染器
void renderShapes(const vector<Shape*>& shapes) {
    for (const auto& shape : shapes) {
        shape->draw(); // 动态绑定
    }
}

int main() {
    Circle circle;
    Rectangle rectangle;

    vector<Shape*> shapes = {&circle, &rectangle};
    renderShapes(shapes);

    return 0;
}

为何实现OCP

  • 添加新形状(如三角形)只需继承 Shape,无需修改现有代码。
  • renderShapes 函数可以处理任何实现了 Shape 的新类,扩展性强。

里氏替换原则 (LSP)

定义:子类必须能够替换掉基类而不影响程序正确性。
目的:保证继承关系的正确性,避免引入不必要的兼容性问题。

示例代码:一个计算四边形面积的系统。

#include <iostream>
using namespace std;

// 基类:四边形
class Quadrilateral {
public:
    virtual double area() const = 0; // 抽象方法:计算面积
    virtual ~Quadrilateral() = default;
};

// 子类:矩形
class Rectangle : public Quadrilateral {
protected:
    double length, width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double area() const override {
        return length * width;
    }
};

// 子类:正方形
class Square : public Rectangle {
public:
    Square(double side) : Rectangle(side, side) {}
};

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

    Quadrilateral* quad = &rect;
    cout << "Rectangle Area: " << quad->area() << endl;

    quad = &square;
    cout << "Square Area: " << quad->area() << endl;

    return 0;
}

为何实现LSP

  • Square 作为 Rectangle 的子类,可以正常替代 Rectangle,满足面积计算逻辑。
  • 程序运行时无需额外判断对象类型,继承关系清晰。

依赖倒置原则 (DIP)

定义:高层模块不依赖于低层模块,二者都依赖于抽象。
目的:减少模块间的耦合,增强系统灵活性。

示例代码:日志记录系统。

#include <iostream>
#include <string>
using namespace std;

// 日志接口
class ILogger {
public:
    virtual void log(const string& message) = 0;
    virtual ~ILogger() = default;
};

// 文件日志实现
class FileLogger : public ILogger {
public:
    void log(const string& message) override {
        cout << "Logging to file: " << message << endl;
    }
};

// 数据库日志实现
class DatabaseLogger : public ILogger {
public:
    void log(const string& message) override {
        cout << "Logging to database: " << message << endl;
    }
};

// 高层模块
class App {
    ILogger& logger; // 依赖抽象 (引用在初始化列表中进行定义)
public:
    App(ILogger& log) : logger(log) {}
    void run() {
        logger.log("Application started");
    }
};

int main() 
{
    FileLogger fileLogger;
    App app(fileLogger); // 注入文件日志
    app.run();

    return 0;
}

为何实现DIP

  • App 不依赖具体日志实现(如文件或数据库),依赖抽象接口(ILogger& logger在构造时候的指向)
  • 日志实现的变化(如添加网络日志)不会影响 App

接口隔离原则 (ISP)

定义:类不应该依赖于它不需要的接口。
目的:避免因为不相关的接口导致类的复杂性增加。

示例代码:打印机接口设计。

#include <iostream>
using namespace std;

// 通用打印机接口
class IPrinter {
public:
    virtual void print() = 0;
    virtual ~IPrinter() = default;
};

// 多功能打印机接口
class IMultiFunctionPrinter : public IPrinter {
public:
    virtual void scan() = 0;
    virtual void fax() = 0;
};

// 普通打印机
class BasicPrinter : public IPrinter {
public:
    void print() override {
        cout << "Basic printing..." << endl;
    }
};

// 多功能打印机
class MultiFunctionPrinter : public IMultiFunctionPrinter {
public:
    void print() override {
        cout << "Printing..." << endl;
    }
    void scan() override {
        cout << "Scanning..." << endl;
    }
    void fax() override {
        cout << "Faxing..." << endl;
    }
};

int main() {
    BasicPrinter printer;
    printer.print();

    return 0;
}

为何实现ISP

  • BasicPrinter 只实现其需要的 IPrinter,而不是实现不需要的 scanfax
  • 避免强迫子类实现不相关的方法,保持接口简洁。

举一个生活中的例子:
可以将接口隔离原则比作电器的遥控器。想象一下,你有一个电视遥控器,如果这个遥控器上不仅可以控制电视的音量和频道,还包含了洗衣机、冰箱和空调的控制按钮,那么它将变得非常复杂和难以使用。对于用户来说,电视遥控器应该只包含与电视控制相关的按钮,而不应该包含其他设备的控制按钮。这就是接口隔离原则的目的:只提供类所需的接口,避免额外的、不需要的复杂性。


优先使用对象组合,而不是类继承

定义:通过组合(has-a 关系)来复用代码,而不是通过继承(is-a 关系)来扩展功能。
目的:避免类继承导致的紧耦合,提升灵活性和扩展性。

可参考3.4依赖倒置原则 (DIP)app高层类的实现

示例代码:文件读取器的实现。

#include <iostream>
#include <string>
using namespace std;

// 文件读取器基类
class FileReader {
public:
    virtual string read() const = 0; // 抽象方法:读取文件
    virtual ~FileReader() = default;
};

// 文本文件读取器
class TextFileReader : public FileReader {
public:
    string read() const override {
        return "Reading text file...";
    }
};

// 文件处理器
class FileProcessor {
    FileReader& reader; // 使用组合,而非继承
public:
    FileProcessor(FileReader& reader) : reader(reader) {}
    void process() {
        cout << reader.read() << endl;
        cout << "Processing file..." << endl;
    }
};

int main() {
    TextFileReader textReader;
    FileProcessor processor(textReader);
    processor.process();

    return 0;
}

为何优先使用组合

  • FileProcessor 使用 FileReader 的接口,通过组合实现功能,而不是继承。
  • 如果需要扩展新的读取方式(如 JSON 文件读取器),只需实现 FileReader 接口,无需修改 FileProcessor

封装变化点

定义:将可能发生变化的部分封装起来,与稳定的部分隔离。
目的:减少变化对系统其他部分的影响。

示例代码:支付系统的实现。

#include <iostream>
#include <string>
using namespace std;

// 支付接口
class PaymentMethod {
public:
    virtual void pay(double amount) const = 0;
    virtual ~PaymentMethod() = default;
};

// 信用卡支付
class CreditCardPayment : public PaymentMethod {
public:
    void pay(double amount) const override {
        cout << "Paying $" << amount << " using Credit Card." << endl;
    }
};

// PayPal支付
class PayPalPayment : public PaymentMethod {
public:
    void pay(double amount) const override {
        cout << "Paying $" << amount << " using PayPal." << endl;
    }
};

// 支付处理器
class PaymentProcessor {
    PaymentMethod& method; // 封装支付方式
public:
    PaymentProcessor(PaymentMethod& method) : method(method) {}
    void processPayment(double amount) {
        method.pay(amount);
    }
};

int main() {
    CreditCardPayment creditCard;
    PayPalPayment paypal;

    PaymentProcessor processor(creditCard);
    processor.processPayment(100);

    return 0;
}

为何封装变化点

  • 支付方式是容易变化的部分,通过抽象 PaymentMethod 隔离变化。
  • 添加新的支付方式(如数字货币支付)时,无需修改 PaymentProcessor

针对接口编程,而不是针对实现编程

定义:依赖于抽象接口,而不是具体实现。
目的:通过接口定义稳定的行为,屏蔽具体实现的变化。

示例代码:日志记录系统。

#include <iostream>
#include <string>
using namespace std;

// 日志接口
class ILogger {
public:
    virtual void log(const string& message) const = 0;
    virtual ~ILogger() = default;
};

// 文件日志实现
class FileLogger : public ILogger {
public:
    void log(const string& message) const override {
        cout << "Logging to file: " << message << endl;
    }
};

// 控制台日志实现
class ConsoleLogger : public ILogger {
public:
    void log(const string& message) const override {
        cout << "Logging to console: " << message << endl;
    }
};

// 应用程序类
class Application {
    ILogger& logger; // 依赖于接口
public:
    Application(ILogger& logger) : logger(logger) {}
    void run() {
        logger.log("Application is running");
    }
};

int main() {
    ConsoleLogger consoleLogger;
    FileLogger fileLogger;

    Application app(consoleLogger);
    app.run();

    return 0;
}

为何针对接口编程

  • Application 仅依赖于 ILogger 接口,屏蔽了具体日志实现的细节。
  • 更换日志实现(如文件日志、网络日志)时无需修改 Application

总结

面向对象设计的八个原则相辅相成,构成了一套强大的设计体系:

原则主要目标
单一职责原则 (SRP)每个类专注于单一职责,降低复杂性和变化影响。
开放封闭原则 (OCP)通过扩展增加功能,对已有代码不做修改。
里氏替换原则 (LSP)确保子类能替代基类,保持程序正确性。
依赖倒置原则 (DIP)高层模块依赖抽象,而非具体实现。
接口隔离原则 (ISP)精简接口,避免强迫类实现不需要的方法。
优先使用对象组合,而不是继承通过组合增强灵活性,避免继承导致的耦合。
封装变化点隔离容易变化的部分,减少变化对系统的影响。
针对接口编程,而不是实现编程依赖抽象接口,屏蔽实现细节。

通过遵循这些原则,可以显著提升代码的复用性、灵活性和可维护性,同时构建一个高内聚、低耦合的系统。


四、面向接口设计

产业发展的标志之一是接口标准化。
例如:

  • 汽车行业:标准化零件设计。
  • 支付行业:支付接口的统一。

在软件开发中,接口设计的标准化也是实现系统协同与复用的关键。

示例:标准化接口
class Payment {
public:
    virtual void processPayment(double amount) = 0;
};

class CreditCardPayment : public Payment {
public:
    void processPayment(double amount) override {
        cout << "Processing credit card payment of $" << amount << endl;
    }
};

class PayPalPayment : public Payment {
public:
    void processPayment(double amount) override {
        cout << "Processing PayPal payment of $" << amount << endl;
    }
};

void processTransaction(Payment& paymentMethod, double amount) {
    paymentMethod.processPayment(amount);
}

五、将设计原则提升为设计经验

  1. 不断实践:通过实际项目理解设计原则的重要性。
  2. 积累模板:将设计原则总结为可复用的模板或模式。
  3. 分享经验:通过团队交流与讨论,让设计经验得到传承和优化。

总结

面向对象设计原则是构建灵活、可扩展系统的基石。通过遵循这些原则,开发者可以有效隔离变化、降低复杂性,同时提升代码的复用性和维护性。在设计模式的学习中,将原则与实际代码结合,才能真正理解面向对象设计的力量。

该专栏之后的文章会对设计模式进行逐个讲解,尽量做到详细通俗,但请保证有一定的语言基础。


http://www.kler.cn/a/427862.html

相关文章:

  • 解决Jupyter Notebook无法转化为Pdf的问题(基于Typora非常实用)
  • shell脚本实战案例
  • 计算机网络复习6——应用层
  • SQL——DQL分组聚合
  • 档案学实物
  • 如何解决‘逻辑删除‘和‘唯一索引‘冲突的问题
  • 七、CNN的变体(更新中)
  • 51c嵌入式~单片机合集3
  • 【jvm】垃圾回收的优点和原理
  • Docker Compose 和 Kubernetes 之间的区别?
  • oracle之用户的相关操作
  • 【C#】键值对的一种常见数据结构Dictionary<TKey, TValue>
  • NAS-FCOS论文总结
  • 【xLSTM-Transformer序列分类】Pytorch使用xLSTM-Transformer对序列进行分类源代码
  • 【Redis集群】使用docker compose创建docker集群,并暴露外部接口
  • Android APP自学笔记
  • 一、web基础和http协议
  • Apache Doris Sql Cache
  • draggable插件——实现元素的拖动排序——拖动和不可拖动的两种情况处理
  • 第一节、电路连接【51单片机-TB6600驱动器-步进电机教程】