面向对象设计原则(solid)
单一职责原则SRP
每个类应该仅有一个职责,换句话说,类应该只做一件事。如果一个类承担了多个责任,未来修改时可能会影响到多个地方,从而引入复杂性和难以维护的问题。
高内聚,低耦合:类与类之间应具有明显的边界,类内的方法间应具有高度相关
几种有问题的情形:
- 如果类名包含“and”和”or“等连接词
class JournalAndPersistenceManager{
void add_entry(std::string);
void save();
};
class UserManagerAndLogger{
void createUser();
void logMessage(String);
};- 类中有多个不相关的方法
class Employee {
private:
std::String name;
double salary;
public:
Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
// 职责1:计算工资
double calculatePay() {
return salary * 1.0;
}
// 职责2:数据库操作
void saveToDatabase() {}
// 职责3:报告生成
std::string generateReport() {
return "员工报告:" + name + ", 工资:" + salary;
}- 修改某个功能时需要修改多个不相关的部分
如果修改数据库存储方式时,需要同时修改工资计算逻辑,那么这些功能可能被错误地放在了同一个类中。
下面是日志和保存的实现
struct Journal
{
std::string title;
std::vector<std::string> entries;
explicit Journal(std::string title)
: title{std::move(title)}
{}
void add(std::string entry)
{
entries.emplace_back(std::move(entry));
}
};
struct PersistenceManager
{
static void save(const Journal& j, const std::string& filename)
{
std::ofstream out(filename);
out << j.title;
for(const auto& e : j.entries)
{
out << e << std::endl;
}
}
};开放封闭原则OCP
软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。也就是说,当需求变化时,可以通过增加新功能(例如,派生类)来扩展现有功能,而不是修改现有代码。
通过扩展现有代码,我们可以增加新的功能或行为,而不会影响到已经稳定运行的代码。
以下BadProductFilter类是不好的实现
#include <iostream>
#include <iterator>
#include <ostream>
#include <string>
#include <vector>
#include <algorithm>
enum class Color
{
Red,
Green,
Blue,
};
std::string to_string(const Color& color)
{
switch(color)
{
case Color::Red:
return "Red";
case Color::Green:
return "Green";
case Color::Blue:
return "Blue";
default:
return "Undefined";
}
}
enum class Size
{
Small,
Medium,
Large,
};
std::string to_string(const Size& size)
{
switch(size)
{
case Size::Small:
return "Small";
case Size::Medium:
return "Medium";
case Size::Large:
return "Large";
default:
return "Undefined";
}
}
struct Product
{
std::string name;
Color color;
Size size;
Product(std::string name, Color color, Size size)
: name{std::move(name)},
color{std::move(color)},
size{std::move(size)}
{}
friend std::ostream& operator<<(std::ostream& out, const Product& product);
};
std::ostream& operator<<(std::ostream& out, const Product& product)
{
return out << to_string(product.size) << " " << to_string(product.color) << " " << product.name;
}
/* Open for modification not extension */
struct BadProductFilter
{
using Items = std::vector<Product*>;
static Items by_color(const Items& items, const Color& color)
{
Items ret;
std::copy_if(items.cbegin(), items.cend(), std::back_inserter(ret), [&color] (const auto* e) { return e->color == color; });
return ret;
};
static Items by_size(const Items& items, const Size& size)
{
Items ret;
std::copy_if(items.cbegin(), items.cend(), std::back_inserter(ret), [&size] (const auto* e) { return e->size == size; });
return ret;
};
static Items by_color_and_size(const Items& items, const Color& color, const Size& size)
{
Items ret;
std::copy_if(items.cbegin(), items.cend(), std::back_inserter(ret),
[&color, &size] (const auto* e)
{
return e->color == color && e->size == size;
});
return ret;
};
};
/* Good way */
template <typename T>
struct ISpecification
{
virtual ~ISpecification() = default;
virtual bool is_satisfied(const T*) const = 0;
};
template <typename T>
struct IFilter
{
virtual ~IFilter() = default;
virtual std::vector<T*> filter(const std::vector<T*>&, const ISpecification<T>&) const = 0;
};
struct GoodFilter : public IFilter<Product>
{
using Items = std::vector<Product*>;
Items filter(const Items& items, const ISpecification<Product>& spec) const override
{
Items result;
std::copy_if(items.cbegin(), items.cend(), std::back_inserter(result),
[&spec] (const auto& e)
{
return spec.is_satisfied(e);
});
return result;
}
};
struct ColorSpecification : public ISpecification<Product>
{
Color color;
explicit ColorSpecification(Color color)
: color{std::move(color)}
{}
bool is_satisfied(const Product* prod) const override
{
return prod->color == color;
}
};
struct SizeSpecification : public ISpecification<Product>
{
Size size;
explicit SizeSpecification(Size size)
: size{std::move(size)}
{}
bool is_satisfied(const Product* prod) const override
{
return prod->size == size;
}
};
template <typename T>
struct CombinedSpecification : public ISpecification<T>
{
ISpecification<T>& first;
ISpecification<T>& second;
explicit CombinedSpecification(ISpecification<T>& first, ISpecification<T>& second)
: first{first},
second{second}
{}
bool is_satisfied(const T* item) const override
{
return first.is_satisfied(item) && second.is_satisfied(item);
}
};
int main()
{
Product apple("Apple", Color::Green, Size::Small);
Product tree("Tree", Color::Green, Size::Large);
Product pool("Pool", Color::Blue, Size::Large);
std::vector<Product*> all_products{&apple, &tree, &pool};
GoodFilter gf;
ColorSpecification green_spec(Color::Green);
auto green_things{gf.filter(all_products, green_spec)};
for(const auto* e : green_things)
{
std::cout << *e << std::endl;
}
return 0;
}里氏替换原则LSP
子类必须能够替代其基类的位置,并且不改变程序的正确性。换句话说,使用基类类型的地方,应该能够使用子类类型而不引入错误。子类在继承父类时,应该保持与父类一致的行为。
这意味着:
- 子类应该完全实现父类的抽象方法
- 子类可以有自己的个性,但不能改变父类的行为
- 子类不应该抛出父类方法没有声明的异常
- 子类的前置条件不应该比父类更严格
- 子类的后置条件不应该比父类更宽松
以下案例是错的
1.子类抛出父类方法没有声明的异常
class fileProcessor
{
public:
virtual void processFile(std::string fileName) {
std::cout << "Processing file " << fileName << std::endl;
}
};
class textFileProcessor : public fileProcessor
{
public:
void processFile(std::string fileName) override {
throw std::runtime_error("textFileProcessor cannot process file " + fileName);
}
};2.子类的前置条件比父类更严格
class textFileProcessor : public fileProcessor
{
public:
void processFile(std::string fileName) override {
if (fileName.size()>10) {
return;
}
std::cout << "Processing text file " << fileName << std::endl;
}
};3.子类的后置条件比父类更宽松
class BankAccount {
public:
BankAccount(double balance) : balance(balance) {}
virtual void deposit(double amount) {
// 存款时更新余额
balance += amount;
}
double getBalance() const { return balance; }
private:
double balance;
};
class LimitedBankAccount : public BankAccount {
public:
LimitedBankAccount(double balance, double limit)
: BankAccount(balance), limit(limit) {}
void deposit(double amount) override {
// 如果存款金额超过了限制,就不进行存款
if (amount > limit) {
std::cerr << "Deposit limit exceeded!" << std::endl;
} else {
BankAccount::deposit(amount);
}
}
private:
double limit;
};接口隔离原则ISP
不应该强迫客户端依赖于它不需要的接口。接口应该小而精,客户端只需要依赖它实际用到的方法。这个原则帮助避免冗余的接口和方法,提高代码的灵活性和可维护性。
比如打印机没有扫描功能,就不应该加上扫描的实现,可以通过组合的形式,而非继承多态。
以下是错的
struct MFP : public IBadMachine
{
void print(const Document&) override
{}
void scan(const Documents&) override
{}
void fax(const Documents&) override
{}
};正确的:
struct IPrinter
{
virtual ~IPrinter() = default;
virtual void print(const std::vector<Document*>&) = 0;
};
struct IScanner
{
virtual ~IScanner() = default;
virtual void scan(const std::vector<Document*>&) = 0;
};
struct Printer : public IPrinter
{
void print(const std::vector<Document*>&) override
{}
};
struct IMachine : public IPrinter, public IScanner
{};
struct Machine : public IMachine
{
IPrinter& printer;
IScanner& scanner;
explicit Machine(IPrinter& printer, IScanner& scanner)
: printer{printer},
scanner{scanner}
{}
void print(const std::vector<Document*>& docs) override
{
printer.print(docs);
}
void scan(const std::vector<Document*>& docs) override
{
scanner.scan(docs);
}
};依赖倒置原则DIP
高层模块不应该依赖低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。通过依赖注入和使用接口或抽象类,来降低高层模块和低层模块之间的耦合性。
#include <iostream>
#include <memory>
using namespace std;
// 抽象通知接口
class INotificationService {
public:
virtual void sendNotification(const string& message) = 0;
virtual ~INotificationService() = default;
};
// 低层模块:邮件通知服务
class EmailService : public INotificationService {
public:
void sendNotification(const string& message) override {
cout << "Sending email: " << message << endl;
}
};
// 低层模块:短信通知服务
class SMSService : public INotificationService {
public:
void sendNotification(const string& message) override {
cout << "Sending SMS: " << message << endl;
}
};
// 高层模块:用户通知服务
class UserNotification {
private:
unique_ptr<INotificationService> notificationService;
public:
UserNotification(unique_ptr<INotificationService> service)
: notificationService(move(service)) {}
void notifyUser(const string& message) {
notificationService->sendNotification(message);
}
};
// 客户端代码
int main() {
// 使用邮件通知服务
unique_ptr<INotificationService> emailService = make_unique<EmailService>();
UserNotification userNotification(move(emailService));
userNotification.notifyUser("Your order has been shipped!");
// 使用短信通知服务
unique_ptr<INotificationService> smsService = make_unique<SMSService>();
UserNotification userNotificationSMS(move(smsService));
userNotificationSMS.notifyUser("Your verification code is 1234.");
return 0;
}其他
- 优先使用对象组合,而不是类继承
- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。
- 继承在某种程度上破坏了封装性,子类父类耦合度高。
- 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
- 封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
- 针对接口编程,而不是针对实现编程
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
- 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。
对象创建
建造者模式
是一种创建型设计模式,允许一步一步地构建复杂对象。它通过分解构建过程,使得对象的构建与其表示分离,从而可以创建不同类型的对象。建造者模式通常用于创建具有复杂结构的对象,并且这个过程需要分步骤完成。
特点:
- 分离对象的创建和表示:建造者模式将对象的创建过程与表示(即对象的具体内容)分开。构建过程与对象的表示是独立的。
- 控制复杂对象的创建过程:通过建造者模式,客户端可以通过不同的建造者来创建复杂的对象,而不需要关心每个步骤的细节。
- 灵活的对象创建:通过多个不同的建造者,可以创建不同的对象表示,客户端只需要选择合适的建造者。
静态工厂方式
静态工厂方法本质上是一种静态方法的应用,用来代替构造函数来创建对象。它可以作为创建对象的一种简化方式,不一定像简单工厂模式那样需要一个额外的工厂类。静态工厂方法可以通过类本身来提供对象的创建,而不需要显式的实例化。
特点:
- 静态方法:通常是静态方法,不需要实例化类即可调用。
- 可以返回不同类型的对象:可以根据不同的条件,返回不同类型的对象(比如不同的子类实例)。
- 没有工厂类的概念:它通常是类本身的一部分,因此没有额外的工厂类存在。
- 灵活性:可以更灵活地管理和控制对象的创建,可以避免每次创建对象时都调用构造函数。
工厂方法/抽象工厂方法
工厂方法(Factory Method)模式是一种创建型设计模式,它提供一个用于创建对象的接口,但让子类决定要实例化的类。工厂方法将类的实例化推迟到子类。换句话说,工厂方法模式允许客户端代码通过接口来创建对象,而不直接依赖于具体的类。
抽象工厂方法(Abstract Factory Method)模式是工厂方法模式的进一步扩展。它提供了一个创建一系列相关或相互依赖对象的接口,而无需指定具体的类。抽象工厂模式的核心思想是多个产品族的创建。每个工厂可以创建多个产品,而每个产品都有多个具体的实现。
工厂模式通常涉及以下几个角色:
抽象工厂(Abstract Factory):定义一个创建对象的接口,但不负责具体的对象创建过程。它通常是一个接口或者抽象类,其中定义了一个或多个创建对象的方法。
具体工厂(Concrete Factory):实现抽象工厂接口,负责实际创建具体的对象。每个具体工厂都对应着一种具体的对象类型。
产品(Product):工厂所创建的对象类型。它可以是一个接口、抽象类或者具体类。
具体产品(Concrete Product):实现产品接口的具体对象。工厂模式的每个具体工厂都负责创建特定的具体产品。

原型模式
通过复制(克隆)现有对象来创建新对象,而不是通过构造函数创建。来避免了重新创建对象的复杂过程。
原型模式非常适合以下场景:
- 对象的创建成本较高:例如,当创建对象需要大量的计算、复杂的初始化过程,或涉及外部资源(如数据库或文件)的交互时,使用原型模式可以显著减少对象创建的成本。
- 对象具有相似性:当需要创建多个相似或相同的对象时,通过克隆一个原型对象来进行快速复制,避免了重复创建的工作。
- 对象的变化不大:对于需要频繁创建且大部分字段相同的对象,可以通过原型模式来减少创建时的额外开销。
单例模式
核心思想是:控制类的实例化过程,确保在整个应用程序中只存在一个实例
常见例子:
- 系统配置管理器:整个系统只需要一个配置管理器
- 日志记录器:应用程序通常只需要一个日志系统
- 缓存管理器:全局缓存管理器应该是唯一的
- 任务管理器:系统任务调度器应该是单例的
享元模式
主要目的是通过共享相同的对象来减少内存使用。该模式特别适用于需要大量相似对象的场景,通过减少对象的数量和内存占用来提高性能。
享元模式的核心思想是:通过共享对象来减少内存的使用。它将对象分为 内部状态 和 外部状态,其中 内部状态 是可以共享的,而 外部状态 是需要根据具体场景变化的
接口隔离
适配器模式
主要目的是将一个类的接口转换成客户端所期望的另一个接口。通过适配器模式,可以使得原本由于接口不兼容而无法一起工作的类能够协同工作。适配器模式通过提供一个转换层来实现兼容性,从而让不同接口的类能够顺利合作。
关键点:
- 目标接口和源接口不兼容,但是需要通过适配器将其进行转换。
- 适配器实现了目标接口,并在内部调用源接口的方法,将不兼容的接口适配为客户端可以使用的形式。
- 装饰器与适配器的区别:装饰器是为了增加新功能,而适配器是为了兼容不同接口。
外观模式
代理模式
主要目的是通过一个代理对象来控制对目标对象的访问。代理模式可以用来控制访问、延迟加载、权限检查、远程通信等功能,它提供了一种替代真实对象的方式,并对目标对象的访问进行封装。
代理模式的关键思想是:用代理对象控制对目标对象的访问,代理对象通常会在调用目标对象的方法前后进行某些额外的操作,如缓存、延迟加载、权限校验等。
代理模式有几种常见的变种,具体包括:
- 虚拟代理:通过延迟初始化(懒加载)来减少不必要的开销,只有当真正需要访问目标对象时才创建它。
- 远程代理:用于表示一个对象在不同的地址空间(如远程服务器)上的实例,通常用来封装远程调用。
- 保护代理:控制对目标对象的访问,根据不同的权限和角色来控制访问,通常用于安全控制。
- 智能代理:提供额外的功能,如内存管理、日志记录、缓存等。
与装饰器模式的区别在于,代理模式侧重于控制,装饰器模式侧重于增加功能
组件协作
模板模式
在父类中定义了算法的骨架,并允许子类在不改变算法结构的前提下重定义算法的某些特定步骤
观察者模式
定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新。观察者模式的核心思想是解耦,即主题(Subject)和观察者(Observer)之间没有直接的依赖关系,主题不需要知道观察者的具体信息,只需要知道观察者实现了统一的接口,可以进行通知。
观察者模式的特点:
- 一对多关系:一个主题(Subject)对象可以有多个观察者(Observers)。当主题发生变化时,它会通知所有观察者。
- 松耦合:主题对象和观察者对象之间通过接口进行交互,而不是直接依赖。主题只负责维护观察者列表,并在状态变化时通知它们,观察者只需要关心何时更新。
- 动态订阅:观察者可以随时注册和注销(订阅和取消订阅)自己,不需要主题对象的重新编译或修改。
策略模式
有一个抽象策略类(接口或抽象类),多个具体策略类实现该接口或继承该类,客户端通过上下文类来选择策略。策略模式解耦了上下文类和具体的算法,客户端只关心算法的接口,而不需要关心算法的具体实现。
单一职责
装饰器模式
用于动态地给对象添加额外的职责。它通过将对象包装在装饰器对象中来扩展对象的功能,而无需修改对象的代码。这使得装饰器模式可以在运行时动态地给对象增加或修改行为,而不会影响到其他对象
关键点:
- 装饰器是通过继承来扩展功能:装饰器类继承了组件接口,可以调用被装饰对象的方法,并在此基础上添加额外的行为。
- 装饰器对象持有原始对象:装饰器类通常包含一个指向原始对象的引用,通过这个引用调用原始对象的方法,然后在此基础上增强或改变功能。
桥接模式
有两个抽象类,一个表示抽象部分,另一个表示实现部分。抽象类通过组合关系持有实现类的实例。将抽象部分和实现部分分离,使它们可以独立变化,从而避免多重继承带来的复杂性。通常通过组合来连接抽象和实现,达到灵活扩展的效果。
数据结构
组合模式
主要目的是将对象组合成树形结构,以表示”部分-整体”的层次结构。组合模式使得客户端可以统一处理单个对象和对象集合(树形结构)之间的差异。它常用于表示树形结构的场景,类似于文件系统中的文件和文件夹结构。
状态变化
状态模式
状态模式是一种
行为型设计模式,它允许对象在其内部状态改变时改变其行为。换句话说,状态模式通过将不同的状态封装成独立的类,使得对象的行为随着状态的变化而变化,从而避免了大量的
if-else 或 switch 语句的嵌套。