【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 的 draw
    drawShape(rectangle); // 调用 Rectangle 的 draw
    return 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 running
    return 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 running
    return 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.mfbz.cn/a/771259.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

二叉树与堆相关的时间复杂度问题

目录 满二叉树与完全二叉树高度h和树中节点个数N的关系 向上调整算法&#xff1a; 介绍&#xff1a; 复杂度推导&#xff1a; 向下调整算法&#xff1a; 介绍&#xff1a; 复杂度推导&#xff1a; 向上调整建堆&#xff1a; 介绍&#xff1a; 复杂度推导&#xff1a;…

9.x86游戏实战-汇编指令mov

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

怎么找到DNS服务器的地址?

所有域都注册到域名名称服务器&#xff08;DNS&#xff09;点&#xff0c;以解析域名应指向的IP地址。此查找类似于在查找个人名称并查找其电话号码时的电话簿如何运行。如果DNS服务器设置错误或指向错误的名称服务器&#xff0c;则域可能无法加载相应的网页。 如何查找当前的…

【python基础】—calendar模块

文章目录 前言一、calendar模块方法1.firstweekday()2.setfirstweekday(firstweekday)3.isleap(year)4.leapdays(y1, y2)5.weekday(year, month, day)6.monthrange(year, month)7.weekheader(n)8.monthcalendar(year, month)9.prmonth(theyear, themonth, w0, l0)10.prcal(year…

堆结构、堆排序

堆 是完全二叉树&#xff0c;类似这种样式的 而这种有右子节点&#xff0c;没左子节点的就不是完全二叉树 分为大根堆和小根堆 大根堆是二叉树里每一颗子树的父节点都是这颗子树里最大的&#xff0c;即每一棵子树最大值是头节点的值 小根堆相反 把数组中从0开始的一段数人…

【等保2.0是什么意思?等保2.0的基本要求有哪些? 】

一、等保2.0是什么意思&#xff1f; 等保2.0又称“网络安全等级保护2.0”体系&#xff0c;它是国家的一项基本国策和基本制度。在1.0版本的基础上&#xff0c;等级保护标准以主动防御为重点&#xff0c;由被动防守转向安全可信&#xff0c;动态感知&#xff0c;以及事前、事中…

Stable Diffusion图像的脸部细节控制——采样器全解析

文章目录 艺术地掌控人物形象好易智算原因分析为什么在使用Stable Diffusion生成全身图像时&#xff0c;脸部细节往往不够精细&#xff1f; 解决策略 局部重绘采样器总结 艺术地掌控人物形象 在运用Stable Diffusion这一功能强大的AI绘图工具时&#xff0c;我们往往会发现自己…

开源的基于图像识别本地实名认证系统(本项目不借助任何api) v1.0

前言: 本项目主要是代替昂贵的实名认证服务api或者sdk&#xff0c;目前仍然存在很多缺点 一、具体介绍 1.组成: 人脸识别服务器分为两部分: (1)、http服务端 server.py共有四个函数: DrawFaceinIdCard:用户上传身份证图片后&#xff0c;服务端会对身份证进行抠人像和ocr处理…

澳蓝荣耀时刻,6款产品入选2024年第一批《福州市名优产品目录》

近日&#xff0c;福州市工业和信息化局公布2024年第一批《福州市名优产品目录》&#xff0c;澳蓝自主研发生产的直接蒸发冷却空调、直接蒸发冷却组合式空调机组、间接蒸发冷水机组、高效间接蒸发冷却空调机、热泵式热回收型溶液调湿新风机组、防火湿帘6款产品成功入选。 以上新…

正交的拉丁方阵(MOLS)

在组合数学中&#xff0c;如果两个同阶的拉丁方阵叠加后&#xff0c;每个位置上的有序对条目都是唯一的&#xff0c;则这两个拉丁方阵被称为正交的。 如果一组同阶的拉丁方阵中&#xff0c;任意两个方阵都是正交的&#xff0c;则这组方阵被称为一组相互正交的拉丁方阵&#xf…

Prometheus 监控Kubelet的运行状态

kubelet通过/metrics暴露自身的指标数据。kubelet有两个端口都提供了这个url&#xff0c;一个是安全端口&#xff08;10250&#xff09;&#xff0c;一个是非安全端口&#xff08;10255&#xff0c;kubeadm安装的集群该端口是关闭的&#xff09;。安全端口使用https协议&#x…

SpringMVC的架构有什么优势?——控制器(一)

文章目录 控制器(Controller)1. 控制器(Controller)&#xff1a;2. 请求映射(Request Mapping)&#xff1a;3. 参数绑定(Request Parameters Binding)&#xff1a;4. 视图解析器(View Resolver)&#xff1a;5. 数据绑定(Data Binding)&#xff1a;6. 表单验证(Form Validation)…

02-部署LVS-DR群集

1.LVS-DR工作原理 LVS-DR模式&#xff0c;Director Server作为群集的访问入口&#xff0c;不作为网购使用&#xff0c;节点Director Server 与 Real Server 需要在同一个网络中&#xff0c;返回给客户端的数据不需要经过Director Server 为了响应对整个群集的访问&#xff0c;…

【JS】过滤数组中空值——arr.filter(Boolean)

前言&#xff1a;过滤数组中的空值&#xff0c;包括 &#xff08;undefined、null、“”、0、false、NaN&#xff09; Boolean函数可以将一个值转换为布尔值&#xff0c;空值会被转换为false&#xff0c;非空值会被转换为true 方法&#xff1a; const arr [1, 2, ""…

Redis 典型应用——分布式锁

一、什么是分布式锁 在一个分布式的系统中&#xff0c;也会涉及到多个节点访问同一个公共资源的情况&#xff0c;此时就需要通过锁来做互斥控制&#xff0c;避免出现类似于 "线程安全" 的问题&#xff1b; 而 Java 中的 synchronized&#xff0c;只能在当前进程中生…

线上问题定位分析宝典——Linux中定位JVM问题常用命令

查询Java进程ID #ps axu | grep java #ps elf | grep java查看机器负载及CPU信息 #top -p 1(进程ID) #top (查看所有进程)获取CPU飙升线程堆栈 1. top -c 找到CPU飙升进程ID&#xff1b; 2. top -Hbp 9702(替换成进程ID) 找到CPU飙升线程ID&#xff1b; 3. $ printf &quo…

ubuntu20.04配置调试工具

1.准备工作&#xff1a;安装g或者gdb sudo apt updatesudo apt install gg --versionsudo apt install gdbgdb --version 2.配置环境 2.1在本地新建一个main.cpp #include <iostream> #include <vector> #include <string>using namespace std;int main(…

【SpringBoot3学习 | 第2篇】SpringBoot3整合+SpringBoot3项目打包运行

文章目录 一. SpringBoot3 整合 SpringMVC1.1 配置静态资源位置1.2 自定义拦截器&#xff08;SpringMVC配置&#xff09; 二. SpringBoot3 整合 Druid 数据源三. SpringBoot3 整合 Mybatis3.1 Mybatis整合3.2 声明式事务整合配置3.3 AOP整合配置 四. SpringBoot3 项目打包和运行…

界面材料知识

界面材料是用于填充芯片和散热器之间的空隙&#xff0c;将低导热系数的空气挤出&#xff0c;换成较高导热系数的材料&#xff0c;以提高芯片散热能力。参考下图 图片来源网上 热阻是衡量界面材料性能最终的参数&#xff0c;其中与热阻有关的有&#xff1a; 1、导热系数&#x…

(三十一)Flask之wtforms库【剖析源码下篇】

每篇前言&#xff1a; &#x1f3c6;&#x1f3c6;作者介绍&#xff1a;【孤寒者】—CSDN全栈领域优质创作者、HDZ核心组成员、华为云享专家Python全栈领域博主、CSDN原力计划作者 &#x1f525;&#x1f525;本文已收录于Flask框架从入门到实战专栏&#xff1a;《Flask框架从入…