文章目录
- 一、面向对象五大原则
- 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); // 假设支付金额为100
return 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
依赖的数据库对象,完美的解决了前面的示例存在的问题,极大的提升了程序的可扩展性。