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

文章目录

  • 面向对象编程的八大原则
    • 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.mzph.cn/pingmian/40596.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自然语言处理学习--3

对自然语言处理领域相关文献进行梳理和总结&#xff0c;对学习的文献进行梳理和学习记录。希望和感兴趣的小伙伴们一起学习。欢迎大家在评论区进行学习交流&#xff01; 论文&#xff1a;《ChineseBERT: Chinese Pretraining Enhanced by Glyph and Pinyin Information》 下面…

原厂商是什么意思?云管平台原厂商有哪些企业?

最近不少IT小伙伴在问关于原厂商相关问题&#xff0c;今天我们就来简单回答一下&#xff0c;仅供参考&#xff01; 原厂商是什么意思&#xff1f; 原厂商&#xff0c;或称原厂&#xff0c;是指生产特定产品或零部件的原始厂家。 软件原厂商是什么意思&#xff1f; 软件原厂…

QT截屏,截取控件为图片,指定范围截屏三种截屏方式

项目中我们常用到截取屏幕&#xff0c;Qt给我的们多种方式&#xff1a; 主要有以下三种&#xff1a; 截取全屏&#xff1b;截取控件为图片&#xff1b;指定位置截屏三种截屏方式&#xff1b; 1.截取全屏 常用&#xff1a; 实现&#xff1a; QScreen *screen QGuiApplicat…

数据结构(一)C语言补

数据结构 内存空间划分 一个进程启动后&#xff0c;会生成4G的内存空间 0~3G是用户空间(应用层) 3~4G是内核空间(底层) 0~3G 3~4G 所有的进程都会共享3G~4G的内核空间&#xff0c; 但是每个进程会独立拥有0~3G的用户空间。 栈区 存放数据特点 栈区存放数据的申请空间的先后…

面试篇-Redis-2+持久化+过期key删除+内存淘汰

文章目录 前言一、你知道Redis 数据是怎么持久化的1.1 Redis 持久化的方式Rdb&#xff1a;1.1.1 主动备份save 命令&#xff1a;1.1.2 Redis 中使用bgsave 进行Rdb 的持久化 &#xff1a; 1.2 Redis 持久化的方式Aof&#xff1a;1.2.1 使用AOF 模式进行数据存储&#xff1a;1.2…

明星代言方式8种助力品牌占领市场-华媒舍

1. 明星代言的重要性和市场价值 明星代言是一种常见的品牌推广方式&#xff0c;通过联系知名度高的明星来推广产品或服务&#xff0c;从而提升品牌的知名度和美誉度。明星代言能够借助明星的影响力和粉丝基础&#xff0c;将品牌信息传达给更广泛的受众&#xff0c;从而提高销量…

Web Based Quiz System v1.0 SQL 注入漏洞(CVE-2022-32991)

前言 CVE-2022-32991 是一个影响 Web Based Quiz System v1.0 的 SQL 注入漏洞。这个漏洞存在于 welcome.php 文件中的 eid 参数处。攻击者可以通过此漏洞在数据库中执行任意 SQL 语句&#xff0c;从而获取、修改或删除数据库中的数据。 具体细节如下&#xff1a; 攻击向量&…

0059__Winsock套接字不能用 _open_osfhandle()函数映射为一个流文件

Winsock套接字不能用_open_osfhandle()函数映射为一个流文件-CSDN博客

Es结合springboot(笔记回忆)

导包 <!--导入es--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency><groupId>org.springframework.boot<…

【代码随想录】【算法训练营】【第53天】 [739]每日温度 [496]下一个更大元素I [503]下一个更大元素II

前言 思路及算法思维&#xff0c;指路 代码随想录。 题目来自 LeetCode。 day 48&#xff0c;周六&#xff0c;不能再坚持~ 题目详情 [739] 每日温度 题目描述 739 每日温度 解题思路 前提&#xff1a;寻找任一个元素的右边比自己大的元素的位置 思路&#xff1a;通常…

jboss 7.2

链接: https://pan.baidu.com/s/19PSAy-Wy8DjcUMy94eqWnw 提取码: rgxf 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 --来自百度网盘超级会员v3的分享链接: https://pan.baidu.com/s/19PSAy-Wy8DjcUMy94eqWnw 提取码: rgxf 复制这段内容后打开百度网盘手机App…

C++:auto命令的含义

在C中&#xff0c;auto 是一个类型说明符&#xff0c;用于自动推断变量的类型。从C11开始&#xff0c;auto 关键字被引入&#xff0c;使得程序员在声明变量时不必明确指定其类型&#xff0c;编译器会根据初始化表达式自动推断出变量的类型。 使用 auto 的好处之一是它可以使代…

激光雷达避障的优缺点

激光雷达避障技术作为一种先进的传感器技术&#xff0c;在多个领域如自动驾驶、机器人导航、安防监控等中得到了广泛应用。以下是激光雷达避障技术的优缺点分析&#xff1a; 一、优点 1.高精度测量&#xff1a;激光雷达能够精确测量物体的距离和位置&#xff0c;对于需要高精度…

quill编辑器使用总结

一、vue-quill-editor 与 quill 若使用版本1.0&#xff0c;这两个组件使用哪个都是一样的&#xff0c;无非代码有点偏差&#xff1b;若需要使用表格功能&#xff0c;必须使用 quill2.0 版本&#xff0c;因为 vue-quill-editor 不支持table功能。 二、webpack版本问题 在使用 q…

软信天成:您的数据仓库真的“达标”了吗?

在复杂多变的数据环境中&#xff0c;您的数据仓库是否真的“达标”了&#xff1f;本文将深入探讨数据仓库的定义、合格标准及其与数据库的区别&#xff0c;帮助您全面审视并优化您的数据仓库。 一、什么是数据仓库&#xff1f; 数据仓库是一个面向主题的、集成的、相对稳定的、…

一个R包完成单细胞基因集富集分析 (全代码)

singleseqgset是用于单细胞RNA-seq数据的基因集富集分析的软件包。它使用简单的基础统计量&#xff08;variance inflated Wilcoxon秩和检验&#xff09;来确定不同cluster中感兴趣的基因集的富集。 Installation library(devtools) install_github("arc85/singleseqgse…

iOS手机竖着拍的照片被旋转了90°的原因以及解决方案

EXIF.getData(IMG_FILE, function () { // IMG_FILE为图像数据 var orientation EXIF.getTag(this, “Orientation”); console.log(“Orientation:” orientation); // 拍照方向 }); 获取拍照方向的结果为1-8的数字&#xff1a; 注意&#xff1a;对于上面的八种方向中&a…

Docker的安装及使用摘要

本文分享一些在docker安装及使用过程中的部分要点&#xff0c;会持续更新&#xff0c;供参考。 1. docker安装 1.1 在ubuntu系统安装 安装指定版本的docker。 # 更新时间&#xff1a;2024年6月23日# docker官方的源无法安装&#xff0c;采用阿里云的源安装docker # 更新软件…

2024.7.4作业

1.梳理笔记(原创) 2. 终端输入一个日期&#xff0c;判断是这一年的第几天 scanf("%d-%d-%d",&y,&m,&d); 闰年2月29天&#xff0c;平年2月28天 #include <stdio.h> int main(int argc,const char *argv[]) { int y0,m0,d0,sum0,i0; …

[论文笔记] pai-megatron-patch Qwen2-72B-CT 后预训练 踩坑记录

经过以下修改,Qwen2-72B-CT可以正常训练,并且benchmark指标和loss正常。 Qwen2-72B-CT开长文本,256卡,16K会OOM,目前能开11K(11008)。 开context parallel需要后续测试。 [论文笔记] Pai-megatron Qwen1.5-14B-CT 后预训练 踩坑记录_pai-megatron-patch 多机-CSDN博客 …