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类违背了里氏替换原则,因为它改变了基类Bird的fly方法的行为。根据里氏替换原则,子类对象应该能够替换父类对象而不改变程序的正确行为。为了避免这种情况,应该确保子类在重写父类的方法时,不会改变其原有的行为契约。
4、接口隔离原则(Interface Segregation Principle, ISP)
不应该强迫客户依赖于它们不使用的方法。这个原则强调接口的粒度。一个接口应该只包含客户需要的方法,避免接口过于庞大和复杂。
示例背景:
示例中
IShape接口包含了三个方法:draw、getArea和getPerimeter。但是,如果有一个只关心形状面积的客户,它不需要实现draw和getPerimeter方法。为了遵循 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依赖的数据库对象,完美的解决了前面的示例存在的问题,极大的提升了程序的可扩展性。
相关文章:
C++ 编程基础(5)类与对象 | 5.8、面向对象五大原则
文章目录 一、面向对象五大原则1、单一功能(Single Responsibility Principle, SRP)2、开放封闭原则(Open/Closed Principle, OCP)3、里氏替换原则(Liskov Substitution Principle, LSP)4、接口隔离原则&am…...
node.js中express的基本了解
定义 Express是基于Node.js平台,快速、开放、极简的Web开发框架。 本质 Express是一个npm上的第三方包,提供了快速创建Web服务器的便捷方法。 作用 与Node.js内置的http模块类似,Express也是专门用来创建Web服务器的,但它极大地简…...
AI大模型(一):Prompt AI编程
一、Prompt Engineering,提示工程 提示工程也叫指令工程: Prompt是发给大模型的指令,比如【讲个睡前故事】、【用Python写个消消乐游戏】等;本质上大模型相关的工程工作,都是围绕prompt展开的;提示工程门…...
ArcGIS Pro属性表乱码与字段名3个汉字解决方案大总结
01 背景 我们之前在使用ArcGIS出现导出Excel中文乱码及shp添加字段3个字被截断的情况,我们有以下应对策略: 推荐阅读:ArcGIS导出Excel中文乱码及shp添加字段3个字被截断? 那如果我们使用ArGIS Pro出现上述问题,该如何…...
小程序-基于java+SpringBoot+Vue的驾校预约平台设计与实现
项目运行 1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境:IDEA,Eclipse,Myeclipse都可以。推荐IDEA; 3.tomcat环境:Tomcat 7.x,8.x,9.x版本均可 4.硬件环境:…...
计算机网络网关简介
网关,在计算机网络中扮演着至关重要的角色,它如同不同语言间的翻译官,让不同网络协议、不同体系结构的网络能够相互通信。简而言之,网关就是一个网络连接到另一个网络的“关口”,负责数据的接收、转换与发送。 在局域…...
如何用python将pdf转换为json格式
使用 Python 将 PDF 文件转换为 JSON 格式,主要步骤如下: 读取 PDF 内容:首先使用一个库读取 PDF 文件内容,如 PyMuPDF 或 pdfplumber。这些库可以逐页提取文本,并返回结构化的数据。 组织数据到 JSON:将提…...
STL关联式容器介绍
在前文中介绍了STL的序列式容器; STL序列式容器之vector-CSDN博客 STL序列式容器之list-CSDN博客 STL序列式容器之deque-CSDN博客 STL序列式容器之stack-CSDN博客 STL序列式容器之queue-CSDN博客 STL序列式容器之heap(堆)-CSDN博客 ST…...
java计算机毕业设计选题参考3000篇
基于微信小程序的springboot高校餐厅食品留样管理系统 springboot vue大学生创新创业训练项目管理系统 Springboot的疫情网课管理系统 基于微信小程序的计算机实验室排课与查询系统ssm后端 基于ssm后端的学生购电电费管理微信小程序weixin356 ssm机场网上订票系统 基于ssmvue的…...
JWT介绍、测试案例 以及实际开发中的使用
什么是JWT? JWT,通过数字签名的方式,以json对象为载体,在不同的服务终端之间安全的传输信息,用来解决传统session的弊端。 JWT在前后端分离系统,通过JSON形式作为WEB应用中的令牌(token),用于…...
快排和归并
目录 前言 快速排序 相遇位置一定比key小的原理(大): 避免效率降低方法(快排优化) 三数取中(选key优化) 小区间优化 hoare版本快排 挖坑法快排 前后指针快排 非递归快排 归并排序 非递…...
VUE+SPRINGBOOT实现邮箱注册、重置密码、登录功能
随着互联网的发展,网站用户的管理、触达、消息通知成为一个网站设计是否合理的重要标志。目前主流互联网公司都支持手机验证码注册、登录。但是手机短信作为服务端网站是需要付出运营商通信成本的,而邮箱的注册、登录、重置密码,无疑成为了这…...
Vue 项目打包后环境变量丢失问题(清除缓存),区分.env和.env.*文件
Vue 项目打包后环境变量丢失问题(清除缓存),区分.env和.env.*文件 问题背景 今天在导报项目的时候遇到一个问题问题:在开发环境中一切正常,但在打包后的生产环境中,某些环境变量(如 VUE_APP_B…...
创建vue+electron项目流程
一个vue3和electron最基本的环境搭建步骤如下:// 安装 vite vue3 vite-plugin-vue-setup-extend less normalize.css mitt pinia vue-router npm create vuelatest npm i vite-plugin-vue-setup-extend -D npm i less -D npm i normalize.css -S ࿰…...
3. 用Ruby on Rails创建一个在线商城
哎呀,你这是想要我写一篇超长篇的Ruby on Rails教程啊!好吧,既然你这么热情,那我就勉为其难地给你来一篇生动有趣、充满比喻夸张讽刺修辞手法的教程吧! 1. 准备工作 1.1. 安装Ruby和Rails 1.1.1 安装Ruby 下载Ruby…...
jmeter常用配置元件介绍总结之配置元件
系列文章目录 1.windows、linux安装jmeter及设置中文显示 2.jmeter常用配置元件介绍总结之安装插件 3.jmeter常用配置元件介绍总结之线程组 4.jmeter常用配置元件介绍总结之函数助手 5.jmeter常用配置元件介绍总结之取样器 6.jmeter常用配置元件介绍总结之jsr223执行pytho…...
SpringBoot获取请求参数
spring boot获取请求参数 文章目录 spring boot获取请求参数一、简单参数二、实体参数三、数组集合参数四、日期参数五、Json参数六、路径参数 开头概述 在Spring Boot框架中,处理HTTP请求并获取请求参数是开发Web应用程序中的一项基本任务。无论是简单的GET请求还是…...
【数据结构】树——顺序存储二叉树
写在前面 在学习数据结构前,我们早就听说大名鼎鼎的树,例如什么什么手撕红黑树大佬呀,那这篇笔记不才就深入浅出的介绍二叉树。 文章目录 写在前面一、树的概念及结构1.1、数的相关概念1.2、数的表示1.3 树在实际中的运用(表示文…...
Android中perform和handle方法的区别——以handleLaunchActivity与performLaunchActivity为例
在Android系统中,perform和handle方法经常出现在关键流程中,分别承担不同的职责。这种命名约定反映了框架设计中的分层思想,帮助开发者区分任务的调度与实现。本文通过handleLaunchActivity和performLaunchActivity这两个典型方法的源码分析&…...
聊聊依赖性测试
在软件测试中,我们常常面临一个挑战:多个模块之间高度耦合,任何一个模块的异常都可能导致整个系统崩溃。如何确保这些模块之间的协作无缝衔接?这就需要依赖性测试的助力! 什么是依赖性测试?它与功能测试、…...
python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...
EtherNet/IP转DeviceNet协议网关详解
一,设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络,本网关连接到EtherNet/IP总线中做为从站使用,连接到DeviceNet总线中做为从站使用。 在自动…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
适应性Java用于现代 API:REST、GraphQL 和事件驱动
在快速发展的软件开发领域,REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名,不断适应这些现代范式的需求。随着不断发展的生态系统,Java 在现代 API 方…...
