【C++设计模式】(一)面向对象编程的八大原则

news/2024/10/5 0:11:56/

文章目录

  • 面向对象编程的八大原则
    • 1 单一职责原则
    • 2 开放-关闭原则
    • 3 里氏替换原则
    • 4 接口隔离原则
    • 5 依赖倒置原则
    • 6 迪米特法则/ 最少知识原则
    • 7 合成复用原则
    • 8 针对接口编程而不是针对实现编程

面向对象编程的八大原则

面向对象编程有一系列的设计准则来保证软件的质量,包括:单一职责原则,开放-关闭原则,里氏替换原则,接口隔离原则,依赖倒置原则,迪米特法则/ 最少知识原则,合成复用原则,针对接口编程而不是针对实现编程原则

1 单一职责原则

单一职责原则强调一个类只负责一个功能,仅有一个引起它变化的原因。这样在修改一个功能时,不会显著影响其他功能。

例如,图书管理员的职责是管理书籍的借还,而不是同时负责打扫卫生和修理设备。

正例:

class Librarian {
public:void manageBooks() {// 管理书籍借还}
};class Cleaner {
public:void cleanLibrary() {// 打扫卫生}
};class Technician {
public:void repairEquipment() {// 修理设备}
};

在这个例子中,每个类都只有一个职责:Librarian 负责管理书籍借还,Cleaner 负责打扫卫生,Technician 负责修理设备。这样职责明确,维护也更加方便。

反例:

class Librarian {
public:void manageBooks() {// 管理书籍借还}void cleanLibrary() {// 打扫卫生}void repairEquipment() {// 修理设备}
};

在这个例子中,Librarian 类不仅负责管理书籍借还,还负责打扫卫生和修理设备,这违反了单一职责原则。这样的类变得复杂且难以维护,更改书籍管理的代码可能会同时影响到清洁和修理设备的功能。

2 开放-关闭原则

开放-关闭原则强调软件实体(类、模块、函数等)应该对扩展开放,对修改关闭,即在有新需求或变化时,通过扩展现有代码来实现新功能,而不是修改原有代码。

例如,插座设计时应该支持不同电器的插头(如电视、冰箱、洗衣机等),我们可以通过增加新的插头适配器来支持新电器,而不需要更改插座本身的设计。

正例:

// 电器接口
class Appliance {
public:virtual void plugIn() = 0; // 虚函数,表示插入电源virtual ~Appliance() {}
};// 电视类
class TV : public Appliance {
public:void plugIn() override {// 电视的插入电源逻辑std::cout << "TV is plugged in." << std::endl;}
};// 冰箱类
class Fridge : public Appliance {
public:void plugIn() override {// 冰箱的插入电源逻辑std::cout << "Fridge is plugged in." << std::endl;}
};// 插座类
class Socket {
public:void plugInAppliance(Appliance* appliance) {appliance->plugIn();}
};int main() {TV tv;Fridge fridge;Socket socket;socket.plugInAppliance(&tv);socket.plugInAppliance(&fridge);return 0;
}

在这个例子中,Appliance 是一个接口(抽象类),TVFridge 是具体实现。Socket 类通过接口来插入不同的电器,从而对扩展开放,对修改关闭。我们可以增加新的电器类,而不需要修改 Socket 类的代码。

反例:

// 插座类直接管理不同电器
class Socket {
public:void plugInTV() {// 电视的插入电源逻辑std::cout << "TV is plugged in." << std::endl;}void plugInFridge() {// 冰箱的插入电源逻辑std::cout << "Fridge is plugged in." << std::endl;}
};int main() {Socket socket;socket.plugInTV();socket.plugInFridge();return 0;
}

在这个例子中,Socket 类直接管理不同电器的插入逻辑。如果要增加新的电器类型,就需要修改 Socket 类的代码,违反了开放-关闭原则。

3 里氏替换原则

里氏替换原则要求子类能够替换其父类并出现在父类能够出现的任何地方,而不引起任何错误或异常。

正例
假设我们有一个基类 Shape,它代表一个形状,并且有一个虚函数 draw() 来绘制形状。然后,我们有两个派生类 CircleRectangle,分别代表圆形和矩形。

#include <iostream>class Shape {
public:virtual void draw() const {std::cout << "Drawing a generic shape." << std::endl;}virtual ~Shape() {}
};class Circle : public Shape {
public:void draw() const override {std::cout << "Drawing a circle." << std::endl;}
};class Rectangle : public Shape {
public:void draw() const override {std::cout << "Drawing a rectangle." << std::endl;}
};void drawShape(const Shape& shape) {shape.draw();
}int main() {Circle circle;Rectangle rectangle;drawShape(circle);  // 调用 Circle 的 drawdrawShape(rectangle); // 调用 Rectangle 的 drawreturn 0;
}

在这个例子中,CircleRectangle 类都重写了 Shape 类的 draw() 方法,且它们的实现都是合理的,没有违反基类 Shape 的任何假设。因此,它们可以被 Shape 类型的引用或指针所替代,并且程序可以正常工作,符合里氏替换原则。

反例

假设我们修改了 Rectangle 类,增加了一个 setHeight() 方法来设置矩形的高度,但在 Shape 基类中并没有这个方法。现在,如果我们尝试在一个只接受 Shape 类型对象的函数中调用 setHeight(),就会出现问题。

class Rectangle : public Shape {
public:void draw() const override {std::cout << "Drawing a rectangle." << std::endl;}// 添加了只在 Rectangle 类中存在的方法void setHeight(int height) {// 设置高度的逻辑}
};void modifyShape(Shape& shape) {// 尝试调用 setHeight(),但 Shape 没有这个方法,因此这里会编译失败shape.setHeight(10);
}int main() {// 尝试使用 modifyShape 将会失败,因为 Shape 没有 setHeight 方法Rectangle rectangle;modifyShape(rectangle);return 0;
}

这里 Rectangle 对象不能被当作 Shape 对象来安全地使用: Shape 类不应该有 setHeight() 方法,那么尝试在 Shape 类型的对象上调用 setHeight()都是不合理的,这违反了里氏替换原则。

4 接口隔离原则

接口隔离原则要求客户端不应该依赖于它不需要的接口,即将大接口拆分为多个小接口,客户端只依赖于它需要的接口。

例如,手机上的应用程序可以只请求访问特定的功能模块(如相机、电话、短信),而不需要整合所有功能模块的超级接口。

正例:

// 功能接口
class Camera {
public:virtual void takePhoto() = 0;virtual ~Camera() {}
};class Phone {
public:virtual void call() = 0;virtual ~Phone() {}
};// 智能手机类,同时继承两个抽象类
class Smartphone : public Phone, public Camera {
public:void call() override {// 手机打电话的逻辑}void takePhoto() override {// 拍照的逻辑}
};int main() {Smartphone phone;Phone* phoneInterface = &phone;Camera* cameraInterface = &phone;phoneInterface->call();        // 正常使用电话功能cameraInterface->takePhoto();  // 正常使用相机功能return 0;
}

在这个例子中,Smartphone 类实现了 PhoneCamera 接口,但客户端可以根据需要选择依赖于 Phone 接口或 Camera 接口,符合接口隔离原则。

反例:

class SuperInterface {
public:virtual void call() = 0;virtual void takePhoto() = 0;
};// 智能手机类
class Smartphone : public SuperInterface {
public:void call() override {// 手机打电话的逻辑}void takePhoto() override {// 拍照的逻辑}
};int main() {Smartphone phone;SuperInterface* superInterface = &phone;superInterface->call();        // 虽然只需要电话功能,但是依然必须实现所有接口中的方法,不符合接口隔离原则。return 0;
}

在这个负面例子中,Smartphone 类实现了一个大接口 SuperInterface,包含了所有功能,尽管客户端只需要电话功能,仍需要实现拍照功能,违反了接口隔离原则。

5 依赖倒置原则

依赖倒置原则 要求高层模块不应依赖于低层模块,而是两者都应该依赖于抽象接口。

例如,USB接口能够连接不同类型的设备(如打印机、键盘、鼠标),这些设备通过共同的接口(USB接口)与电脑通信,而不需要知道每个设备的具体实现细节。

正例:

// 抽象设备接口
class Device {
public:virtual void operate() = 0;  // 设备操作的抽象方法virtual ~Device() {}
};// 具体设备类:打印机
class Printer : public Device {
public:void operate() override {// 打印机操作的具体实现std::cout << "Printer is printing." << std::endl;}
};// 电脑类,依赖于设备接口
class Computer {
private:Device* device;  // 电脑依赖于抽象的设备接口
public:Computer(Device* dev) : device(dev) {}void operateDevice() {device->operate();  // 通过抽象接口操作设备}
};int main() {Printer printer;Computer computer(&printer);computer.operateDevice();  // 电脑操作打印机return 0;
}

在这个例子中,Computer 类通过抽象的 Device 接口依赖于具体的 Printer 类,符合依赖倒置原则。

反例:

class Printer {
public:void operate() {// 打印机操作的具体实现std::cout << "Printer is printing." << std::endl;}
};// 电脑类,直接依赖于具体的打印机类
class Computer {
private:Printer printer;  // 电脑直接依赖于具体的打印机类
public:void operateDevice() {printer.operate();  // 直接操作打印机}
};int main() {Computer computer;computer.operateDevice();  // 电脑直接操作打印机,违反了依赖倒置原则。return 0;
}

在这个负面例子中,Computer 类直接依赖于具体的 Printer 类,如果需要改变打印机为其他设备,就需要修改 Computer 类的代码,违反了依赖倒置原则。

6 迪米特法则/ 最少知识原则

最少知识原则要求一个对象应该尽可能少地了解其他对象,只和与之直接交互的对象(中介)通信,减少对象之间的耦合度。

例如,公司老板只通过秘书与外部供应商进行沟通,而不直接与供应商交流。

正例:

#include <iostream>
#include <string>// 供应商类
class Supplier {
public:void supply(const std::string& item) {std::cout << "Supplying " << item << "." << std::endl;}
};// 秘书类
class Secretary {
private:Supplier* supplier;  // 秘书知道供应商的存在,但不直接与供应商交互 
public:Secretary(Supplier* s) : supplier(s) {}void orderItem(const std::string& item) {supplier->supply(item);  // 通过供应商供货}
};// 老板类
class Boss {
private:Secretary* secretary;  // 老板只通过秘书与供应商交互
public:Boss(Secretary* sec) : secretary(sec) {}void placeOrder(const std::string& item) {secretary->orderItem(item);  // 老板通过秘书订购物品}
};int main() {Supplier supplier;Secretary secretary(&supplier);Boss boss(&secretary);boss.placeOrder("500 units of paper");return 0;
}

在这个例子中,Boss 类只通过 Secretary 类与 Supplier 类进行交互,符合最少知识原则。

反例:

class Boss {
public:void placeOrder(Supplier* supplier, const std::string& item) {supplier->supply(item);  // 老板直接与供应商交互,违反了最少知识原则}
};int main() {Supplier supplier;Boss boss;boss.placeOrder(&supplier, "500 units of paper");return 0;
}

在这个负面例子中,Boss 类直接与 Supplier 类交互,违反了最少知识原则。如果 Supplier 类的实现发生变化,那么 Boss 类也可能需要进行相应的修改,这增加了系统的维护成本和复杂度。

7 合成复用原则

合成复用原则强调尽量使用对象组合而不是继承来实现复用,通过将已有的对象纳入新对象中,作为新对象的成员变量来实现新功能。

如果子类和父类之间存在明显的“是一个(IS-A)”关系,即子类是父类的一种类型,可以使用继承。例如,Dog继承自Animal,因为狗是一种动物。但是,如果类之间存在明显的“有一个(HAS-A)”关系,即一个类拥有另一个类的实例,则应该使用合成。例如,Car有一个Engine,所以Car类可以包含一个Engine类的实例。

正例:

#include <iostream>class Engine {
public:void start() {std::cout << "Engine started" << std::endl;}
};class Car {
private:Engine engine; // Car拥有一个Engine对象public:void start() {engine.start(); // 调用Engine的start方法std::cout << "Car is now running" << std::endl;}
};int main() {Car myCar;myCar.start(); // 输出:Engine started 和 Car is now runningreturn 0;
}

在这个例子中,Car类没有继承自Engine类,而是将Engine类的实例作为自己的成员变量。这样,Car类就复用了Engine类的功能,同时保持了类的独立性和封装性。

反例:

#include <iostream>class Engine {
protected:void start() { // 注意这里改为protected,以便子类可以访问std::cout << "Engine started" << std::endl;}
};// 错误地通过继承复用Engine的功能
class Car : public Engine {
public:void startCar() {start(); // 调用从Engine继承来的start方法std::cout << "Car is now running" << std::endl;}
};int main() {Car myCar;myCar.startCar(); // 输出:Engine started 和 Car is now runningreturn 0;
}

虽然这个反例在技术上可行,但它破坏了类的封装性和独立性。Car类现在“是一个”Engine,这在现实中显然是不合理的。此外,如果Engine类有其他与汽车无关的功能或属性,那么这些都会被Car类继承,从而导致不必要的复杂性和潜在的错误。

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

针对接口编程,而不是针对实现编程,指的是在编程时应依赖于抽象接口,而不是具体实现。

假设你正在开发一个游戏,其中有一个角色系统。游戏中的角色可以有不同的类型,比如战士、法师和盗贼,每种角色都有自己独特的技能。

正例

// Character 接口
class Character {
public:virtual ~Character() {} // 虚析构函数virtual void attack() = 0; // 纯虚函数,要求子类必须实现virtual void defend() = 0; // 纯虚函数,要求子类必须实现
};// 战士类实现 Character 接口
class Warrior : public Character {
public:void attack() override {std::cout << "Warrior attacks fiercely!" << std::endl;}void defend() override {std::cout << "Warrior defends with shield." << std::endl;}
};// 角色系统使用接口编程
void battle(Character& character) {character.attack();character.defend();
}int main() {Warrior warrior;battle(warrior); // 传入 Warrior 对象,展示如何使用接口编程return 0;
}

如果我们针对一个接口(或基类)来编程,比如定义一个Character接口,然后让战士、法师、盗贼等角色类都实现这个接口,那么我们就可以在不修改已有代码的情况下,通过添加新的角色类或者修改接口的实现来扩展或修改游戏的行为。

反例

// 直接使用具体类,没有接口
class Warrior {
public:void warriorAttack() {std::cout << "Warrior attacks fiercely!" << std::endl;}void warriorDefend() {std::cout << "Warrior defends with shield." << std::endl;}
};// 角色系统直接使用 Warrior 类
void warriorBattle(Warrior& warrior) {warrior.warriorAttack();warrior.warriorDefend();
}int main() {Warrior warrior;warriorBattle(warrior); // 如果添加新的角色类型,比如法师,需要修改 warriorBattle 或添加新的函数return 0;
}

如果我们直接针对每种角色的具体实现(即战士类、法师类、盗贼类)来编程,那么当我们需要添加一个新的角色类型或者修改某个角色的行为时,可能需要修改大量已经存在的代码。例如,如果我们需要添加一个新的角色类型(如法师),我们就需要修改warriorBattle函数或者创建一个新的函数,这增加了代码的复杂性和维护成本。


http://www.ppmy.cn/news/1473506.html

相关文章

C#——异步Task详情

C#异步Task 异步&#xff1a;多任务开始执行&#xff0c;只需要主任务 A 执行完成就算结束&#xff0c;主任务执行的时候&#xff0c;可以同时执行异步任务 B、C&#xff0c;主任务 A 可以不需要等待异步任务 B、C 的结果。 在C#中&#xff0c;异步编程主要通过async和await关…

MySQL——JDBC中对象解释

DriverManager Class.forName("com.mysql.cj.jdbc.Driver"); // 固定写法&#xff0c;加载驱动 Connection connection DriverManager.getConnection(url, username, password); // Connection:代表数据库 // 数据库设置自动提交 // 事务提交 // 自动回滚 connecti…

C#——MD5 base64加密-base64加密解密

MD5 base64加密 在C#中&#xff0c;MD5是一个不可逆的加密算法&#xff0c;因为它是散列函数&#xff0c;用于创建信息的唯一指纹&#xff08;也称为摘要&#xff09;。不过&#xff0c;可以使用MD5进行加密&#xff0c;然后使用Base64对结果进行编码。但是&#xff0c;请注意…

Vatee万腾平台:智慧生活的无限可能

在科技日新月异的今天&#xff0c;我们的生活正被各种智能技术悄然改变。从智能家居到智慧城市&#xff0c;从个人健康管理到企业数字化转型&#xff0c;科技的力量正以前所未有的速度渗透到我们生活的每一个角落。而在这场智能革命的浪潮中&#xff0c;Vatee万腾平台以其卓越的…

wireshark与tcpdump使用

文章目录 wireshark与tcpdump使用tcpdump过滤expression表达式wireshark的显示过滤器tcpdump语法wireshark与tcpdump使用 tcpdump过滤 官网:http://www.tcpdump.org 需要安装libpcap # 示例 tcpdump -vv -i any port 80 or port 443 -s0 -w /home/tkg.pcapexpression表达式…

Lianwei 安全周报|2024.07.01

新的一周又开始了&#xff0c;以下是本周「Lianwei周报」&#xff0c;我们总结推荐了本周的政策/标准/指南最新动态、热点资讯和安全事件&#xff0c;保证大家不错过本周的每一个重点&#xff01; 政策/标准/指南最新动态 01 出于安全考虑&#xff0c;拜登下令禁用卡巴斯基杀毒…

爬虫笔记0

问题梳理: <dl>&#xff1a;Definition List&#xff08;定义列表&#xff09; <dt>&#xff1a;Definition Term&#xff08;一般放标题&#xff09; <dd>&#xff1a;Definition Description&#xff08;定义列表项&#xff0c;数据所在&#xff09; &l…

Kafka如何防止消息丢失

持久化存储&#xff1a; Kafka 使用基于日志的存储模型。每个主题的分区都会被分割成多个片段&#xff08;Segment&#xff09;&#xff0c;每个片段都是一个可追加的日志文件。生产者发送的每条消息都会被追加到对应分区的当前活跃片段&#xff08;active segment&#xff09;…