C++ 编程基础(5)类与对象 | 5.8、面向对象五大原则

文章目录

  • 一、面向对象五大原则
    • 1、单一功能(Single Responsibility Principle, SRP)
    • 2、开放封闭原则(Open/Closed Principle, OCP)
    • 3、里氏替换原则(Liskov Substitution Principle, LSP)
    • 4、接口隔离原则(Interface Segregation Principle, ISP)
    • 5、依赖倒置原则(Dependency Inversion Principle, DIP)

前言:

在软件开发领域,面向对象编程(OOP)是一种重要的编程范式,它通过封装、继承和多态等特性,提高了代码的可重用性、灵活性和可维护性。C++作为一种强大的面向对象编程语言,充分体现了这些原则。在面向对象的设计中,有五大核心原则被广泛认可和应用,它们分别是:单一职责原则(SRP)、开放封闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖倒置原则(DIP)。下面,将逐一解析这五大原则在C++中的应用。

一、面向对象五大原则

1、单一功能(Single Responsibility Principle, SRP)

一个类应该只有一个引起变化的原因,即一个类只负责一项职责。这个原则强调类的专注性,避免一个类承担过多的责任。当一个类承担多个职责时,其内聚力会降低,代码的可读性和可维护性也会受到影响。

2、开放封闭原则(Open/Closed Principle, OCP)

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这个原则鼓励通过继承和多态来实现功能的扩展,而不是通过修改现有代码。这样可以在不改变原有代码的基础上添加新功能,提高系统的灵活性和可维护性。

示例背景:

假设有一个支付系统,最初只支持信用卡支付。随着业务的发展,需要添加对其他支付方式的支持,如 PayPal 和比特币。为了遵循开放封闭原则,可以设计一个抽象的支付接口,然后为每种支付方式创建具体的实现类。这样,当需要添加新的支付方式时,只需要创建一个新的实现类并将其添加到系统中即可,无需修改现有的代码。

#include <iostream>
#include <string>
#include <vector>// 抽象的支付接口
class IPayment {
public:virtual void pay(double amount) = 0;virtual ~IPayment() {}
};// 信用卡支付的具体实现
class CreditCardPayment : public IPayment {
public:void pay(double amount) override {std::cout << "Paying " << amount << " using Credit Card." << std::endl;}
};// PayPal支付的具体实现
class PayPalPayment : public IPayment {
public:void pay(double amount) override {std::cout << "Paying " << amount << " using PayPal." << std::endl;}
};// 比特币支付的具体实现
class BitcoinPayment : public IPayment {
public:void pay(double amount) override {std::cout << "Paying " << amount << " using Bitcoin." << std::endl;}
};// 支付处理类
class PaymentProcessor {
private:std::vector<IPayment*> payments;
public:void addPaymentMethod(IPayment* payment) {payments.push_back(payment);}void processPayments(double amount) {for (IPayment* payment : payments) {payment->pay(amount);}}~PaymentProcessor() {for (IPayment* payment : payments) {delete payment;}}
};int main() {// 创建支付处理器PaymentProcessor processor;// 添加不同的支付方式processor.addPaymentMethod(new CreditCardPayment());processor.addPaymentMethod(new PayPalPayment());processor.addPaymentMethod(new BitcoinPayment());// 处理支付processor.processPayments(100.0); // 假设支付金额为100return 0;
}

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

子类型必须能够替换掉它们的基类型。这个原则强调继承关系中的一致性。如果一个派生类不能替代其基类而不改变程序的正确性,那么这个继承关系就是不合理的。

示例背景:

下面给出一个违反里氏替换原则的示例,假设有一个基类 Bird 和一个派生类 Penguin,如下:

#include <iostream>
using namespace std;class Bird {
public:virtual void fly() {cout << "I can fly!" << endl;}
};class Penguin : public Bird {
public:void fly() override {cout << "I cannot fly!" << endl;}
};

在这个例子中,Bird 类有一个 fly 方法,该方法输出 I can fly!Penguin 类继承自 Bird 并重写了 fly 方法,输出 I cannot fly!

在这个例子中,Penguin 类违背了里氏替换原则,因为它改变了基类 Birdfly 方法的行为。根据里氏替换原则,子类对象应该能够替换父类对象而不改变程序的正确行为。为了避免这种情况,应该确保子类在重写父类的方法时,不会改变其原有的行为契约。

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

不应该强迫客户依赖于它们不使用的方法。这个原则强调接口的粒度。一个接口应该只包含客户需要的方法,避免接口过于庞大和复杂。

示例背景:

示例中 IShape 接口包含了三个方法:drawgetAreagetPerimeter。但是,如果有一个只关心形状面积的客户,它不需要实现 drawgetPerimeter 方法。为了遵循 ISP,可以将接口拆分为更小的接口。

class IShape {
public:virtual void draw() const = 0;virtual int getArea() const = 0;virtual int getPerimeter() const = 0;
};class Circle : public IShape {
public:void draw() const override { /* ... */ }int getArea() const override { /* ... */ }int getPerimeter() const override { /* ... */ }
};

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

高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。这个原则强调通过抽象来解耦模块之间的依赖关系。高层模块应该依赖于抽象接口,而不是具体的实现类。这样可以提高系统的灵活性和可扩展性。

示例背景:

假设现在要做一个电商系统,需要实现的基本功能是订单入库。

版本一:违反依赖倒置原则

假设系统设计初期,用的是SQL Server数据库。通常会定义一个SqlServer类,用于数据库的读写。然后定义一个Order类,负责订单的逻辑处理。由于订单要入库,需要依赖于数据库的操作。因此在Order类中,需要定义SqlServer类的变量并初始化。

// 定义SqlServer类负责与数据库进行交互
class SqlServer {
public:void add() {cout<<"往数据库添加一个订单."<<endl;}
};// 定义Order类处理业务,并使用SqlServer类提供的能力,实现订单入库的功能
class Order {
private:SqlServer *p;
public:Order() {p = new SqlServer;}void add() {// 先进行订单的逻辑处理,再把这个订单放到数据库p->add();}
};

如果要使用Oracle数据库,那么要重新写一个OracleServer类,然后对Order类进行修改,程序扩展性比较差。上面程序扩展性不强的原因主要有下面两个

  • Order直接依赖于一个具体的类。
  • Order依赖的对象的创建与绑定是在它的内部实现的。

下面的示例重点分析了下如何解决这两个问题

版本二:符合依赖倒置原则

为了解决Order直接依赖于一个具体的类的问题,可以定义一个抽象类DataAccess,类DataAccess提供了操作数据库的接口,Order类依赖抽象类DataAccess,如下:

class DataAccess {
public:virtual void add() {}
} ;class SqlServer : public DataAccess {
public:void add() {cout<<"往 SQL 数据库添加一个订单."<<endl;}
};class Oracle : public DataAccess {
public:void add() {cout<<"往 Oracle 数据库添加一个订单."<<endl;}
};class Order {
private:DataAccess &re;
public:Order(DataAccess &re):re(re) {}void add() {// 先进行订单的逻辑处理,再把这个订单放到数据库re.add();}
};

通过控制反转(Inversion of Control,缩写为IoC)可以解决前面的第二个问题,下面先介绍下什么是控制反转,以及如何实现控制反转。

控制反转:

  • 定义: 控制反转是一种设计思想,它将对象的控制权从代码本身转移到外部容器或框架中。具体来说,在采用控制反转之前,对象通常会自己负责创建并管理它所依赖的其他对象。而在控制反转中,对象的依赖关系会在其创建时或运行时由外部实体(如IoC容器)注入。
  • 实现方式: 控制反转最常见的实现方式是依赖注入(Dependency Injection,简称DI)。依赖注入允许在运行时动态地将依赖关系注入到对象中,从而降低了对象之间的耦合度。依赖注入有多种实现形式,包括:
    • 构造器注入: 通过构造器将依赖对象传递给被依赖的对象。
    • Setter方法注入: 通过Setter方法将依赖对象设置到被依赖的对象中。
    • 接口注入: 通过接口将依赖对象注入到被依赖的对象中。

可以通过构造函数,将Order依赖的数据库对象注入给它,如下:

class Order{
private:DataAccess &re;
public:// 通过构造函数接受依赖的数据库对象Order(DataAccess &re):re(re) {}void add() {// 先进行订单的逻辑处理,再把这个订单放到数据库re.add();}
};int main() {SqlServer sql;         // 在外部创建依赖对象Order order1(sql);     // 通过构造函数注入依赖order1.add();Oracle oracle;         // 在外部创建依赖对象Order order2(oracle);  // 通过构造函数注入依赖order2.add();return 0;
}

Order依赖抽象类DataAccess以及通过构造函数来注入Order依赖的数据库对象,完美的解决了前面的示例存在的问题,极大的提升了程序的可扩展性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/886206.shtml

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

相关文章

计算机网络中的域名系统(DNS)及其优化技术

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 计算机网络中的域名系统&#xff08;DNS&#xff09;及其优化技术 计算机网络中的域名系统&#xff08;DNS&#xff09;及其优化…

STM32单片机CAN总线汽车线路通断检测

目录 目录 前言 一、本设计主要实现哪些很“开门”功能&#xff1f; 二、电路设计原理图 1.电路图采用Altium Designer进行设计&#xff1a; 2.实物展示图片 三、程序源代码设计 四、获取资料内容 前言 随着汽车电子技术的不断发展&#xff0c;车辆通信接口在汽车电子控…

(实战)WebApi第13讲:怎么把不同表里的东西,包括同一个表里面不同的列设置成不同的实体,所有的给整合到一起?【前端+后端】、前端中点击标签后在界面中显示

一、实现全局跨域&#xff1a;新建一个Controller&#xff0c;其它的controller都继承它 1、新建BaseController 2、在后端配置&#xff0c;此处省略【详情见第12讲四、3、】 3、其它的控制器继承BaseController&#xff0c;这个时候就能够完成全局的跨域 【向后台传cookie和…

前缀和技巧解析

前缀和技巧解析 前缀和&#xff08;Prefix Sum&#xff09;是一种常用的算法技巧&#xff0c;用于高效地处理一系列连续子数组和的问题。通过构建一个额外的数组来存储从数组起始位置到当前位置的累计和&#xff0c;可以在常数时间内快速计算任意区间的和。 前缀和应用的典型…

Mysql每日一题(行程与用户,困难※)

今天给大家分享一个截止到目前位置&#xff0c;我遇到最难的一道mysql题目&#xff0c;非常建议大家亲手做一遍 完整代码如下&#xff0c;这道题的主要难点是它有两个外键&#xff0c;以前没遇到过&#xff0c;我也没当回事&#xff0c;分享一下错误经验哈 当时我写的where判断…

已解决:spark代码中sqlContext.createDataframe空指针异常

这段代码是使用local模式运行spark代码。但是在获取了spark.sqlContext之后&#xff0c;用sqlContext将rdd算子转换为Dataframe的时候报错空指针异常 Exception in thread "main" org.apache.spark.sql.AnalysisException: java.lang.RuntimeException: java.lang.Nu…

cooladmin 后端 查询记录

查询记录&#xff1a;pageQueryOp中列表查询的group by node ts controller代码如下 import { CoolController, BaseController } from cool-midway/core; import { Inject, Post, Get, Param } from midwayjs/decorator; import { ComparePricesPlanInfoEntity } from ../../…

cesium 3DTiles之pnts格式详解

Point Cloud 1 概述 点云&#xff08;Point Cloud&#xff09;瓦片格式用于高效流式传输大规模点云数据&#xff0c;常用于 3D 可视化中。每个点由位置&#xff08;Position&#xff09;和可选的属性定义&#xff0c;这些属性用来描述点的外观&#xff08;如颜色、法线等&…

【SpringBoot】20 同步调用、异步调用、异步回调

Git仓库 https://gitee.com/Lin_DH/system 介绍 同步调用&#xff1a;指程序在执行时&#xff0c;调用方需要等待函数调用返回结果后&#xff0c;才能继续执行下一步操作&#xff0c;是一种阻塞式调用。 异步调用&#xff1a;指程序在执行时&#xff0c;调用方在调用函数后立…

ESLint 使用教程(五):ESLint 和 Prettier 的结合使用与冲突解决

系列文章 ESLint 使用教程&#xff08;一&#xff09;&#xff1a;从零配置 ESLint ESLint 使用教程&#xff08;二&#xff09;&#xff1a;一步步教你编写 Eslint 自定义规则 ESLint 使用教程&#xff08;三&#xff09;&#xff1a;12个ESLint 配置项功能与使用方式详解 ES…

Qt_day5_常用类

常用类 目录 1. QString 字符串类&#xff08;掌握&#xff09; 2. 容器类&#xff08;掌握&#xff09; 2.1 顺序容器QList 2.2 关联容器QMap 3. 几种Qt数据类型&#xff08;熟悉&#xff09; 3.1 跨平台数据类型 3.2 QVariant 统一数据类型 3.3 QStringList 字符串列表 4. QD…

VBA学习笔记:基础知识

1.打开编辑器 工具-选项&#xff0c;可设置编辑器字体大小等 2. 运行 快捷键F5&#xff0c;或 运行-运行宏 若提示宏被禁止&#xff0c;解决办法之一&#xff1a;工具-宏-安全性-安全级-中&#xff0c;关闭excel重新打开&#xff0c;启用宏 保存文件格式为xla或xlam 3. 基本…

【CANOE】【学习】【DecodeString】字节转为中文字符输出

系列文章目录 文章目录 系列文章目录前言一、DecodeString 转为中文字节输出二、代码举例1.代码Demo2.DecodeString 函数说明函数语法&#xff1a;参数说明&#xff1a;返回值&#xff1a;使用示例&#xff1a;示例代码&#xff1a; 说明&#xff1a; 前言 有时候使用的时候&a…

超好用shell脚本NuShell mac安装

利用管道控制任意系统 Nu 可以在 Linux、macOS 和 Windows 上运行。一次学习&#xff0c;处处可用。 一切皆数据 Nu 管道使用结构化数据&#xff0c;你可以用同样的方式安全地选择&#xff0c;过滤和排序。停止解析字符串&#xff0c;开始解决问题。 强大的插件系统 具备强…

【Window主机访问Ubuntu从机——Xrdp配置与使用】

使用Xrdp在Window环境下远程桌面访问Ubuntu主机 文章目录 Ubuntu安装图形化界面Ubuntu安装Xrdp通过网线连接两台主机Window主机有线连接配置Ubuntu从机设置测试有线连接 Window主机打开远程桌面功能参考文章总结 Ubuntu安装图形化界面 sudo apt update sudo apt upgrade sudo …

ECharts图表图例8

用eclipse软件制作动态单仪表图 用java知识点 代码截图&#xff1a;

实验6记录网络与故障排除

实验6记录网络与故障排除 实验目的及要求&#xff1a; 通过实验&#xff0c;掌握如何利用文档记录网络设备相关信息并完成网络拓扑结构的绘制。能够使用各种技术和工具来找出连通性问题&#xff0c;使用文档来指导故障排除工作&#xff0c;确定具体的网络问题&#xff0c;实施…

读取文件内容、修改文件内容、识别文件夹目录(Web操作系统文件/文件夹详解)

前言 因 Unicode IDE 编辑器导入文件、文件夹需要&#xff0c;研究了下导入文件/文件夹的功能实现&#xff0c;发现目前相关文章有点少&#xff0c;故而记录下过程&#xff0c;如果有误&#xff0c;还望指正。(API的兼容性及相关属性、接口定义&#xff0c;请自行查看文件系统 …

【卡尔曼滤波】数据融合Fusion的应用 C语言、Python实现(Kalman Filter)

【卡尔曼滤波】数据融合Fusion的应用 C语言、Python实现&#xff08;Kalman Filter&#xff09; 更新以gitee为准&#xff1a; gitee地址 文章目录 卡尔曼滤波数据融合Python实现C语言实现多个数据如何融合附录&#xff1a;压缩字符串、大小端格式转换压缩字符串浮点数压缩Pac…

docker-hub 无法访问,使用windows魔法拉取docker images再上传到linux docker环境中

云机的服务器是可以docker拉取镜像的&#xff0c;但是本地的虚拟机、物理服务器等网络环境不好的情况&#xff0c;是无法访问docker-hub的&#xff0c;即使更换了docker镜像源国内源也无法使用。 本文章使用 在魔法网络环境下的windows&#xff0c;下载docker images后&#xf…