设计模式与惯用法:构建高质量软件的关键
1 设计原则与设计模式的区别
在软件开发中,设计原则和设计模式是两个核心概念。设计原则是指导软件设计的基本准则,它们通常独立于特定的编程范式和技术。例如,KISS(保持简单和直接)、DRY(不要重复自己)、YAGNI(你不会需要它)、单一职责原则、开闭原则和信息隐藏等原则,无论是在面向对象编程还是函数式编程中,都是适用的。这些原则帮助开发者编写简洁、可维护且易于扩展的代码。
设计模式则是针对特定上下文中具体设计问题的解决方案。它们通常与面向对象编程紧密相关,例如,《设计模式:可复用面向对象软件的基础》一书中描述的23种经典设计模式。设计模式提供了经过验证的解决方案,帮助开发者应对常见的设计挑战。设计原则更为持久且重要,因为它们是设计模式的基础。掌握这些原则后,开发者可以更好地理解和应用设计模式。
2 常用的设计模式及其应用场景
2.1 依赖注入 (Dependency Injection, DI)
依赖注入是一种解耦组件依赖关系的技术,使得组件不必直接创建或查找其依赖项。通过外部提供依赖项,组件可以专注于其核心功能,而不必关心依赖项的生命周期管理。DI的核心思想是将依赖项的创建和管理移交给外部容器,从而使代码更加模块化和易于测试。
实现依赖注入的方法
- 构造函数注入 :依赖项通过构造函数传递给组件。这种方式确保了组件在构造时就被完全初始化,之后可以直接使用。
- Setter注入 :依赖项通过setter方法传递给组件。这种方式适用于运行时动态注入依赖项的情况。
示例代码
class Logger final {
public:
static Logger& getInstance() {
static Logger theLogger{};
return theLogger;
}
void writeInfoEntry(std::string_view entry) {
// ...
}
void writeWarnEntry(std::string_view entry) {
// ...
}
void writeErrorEntry(std::string_view entry) {
// ...
}
};
class Customer {
public:
Customer() = default;
void setLoggingService(const Logger& loggingService) {
logger = loggingService;
}
private:
Logger logger;
};
2.2 适配器 (Adapter)
适配器模式用于将一个类的接口转换成客户端期望的另一个接口,从而使原本由于接口不兼容而不能一起工作的类可以协同工作。适配器模式常用于集成第三方库或不同团队开发的模块,确保这些模块能够无缝协作。
示例代码
class LoggingFacility {
public:
virtual void writeInfoEntry(std::string_view entry) = 0;
virtual void writeWarnEntry(std::string_view entry) = 0;
virtual void writeErrorEntry(std::string_view entry) = 0;
};
class BoostLogAdapter : public LoggingFacility {
public:
virtual void writeInfoEntry(std::string_view entry) override {
BOOST_LOG_TRIVIAL(info) << entry;
}
virtual void writeWarnEntry(std::string_view entry) override {
BOOST_LOG_TRIVIAL(warn) << entry;
}
virtual void writeErrorEntry(std::string_view entry) override {
BOOST_LOG_TRIVIAL(error) << entry;
}
};
2.3 策略 (Strategy)
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以互换。策略模式允许算法独立于使用它的客户端变化,从而提高了代码的灵活性和可扩展性。策略模式非常适合处理需要根据不同条件选择不同算法的场景,例如排序算法的选择。
示例代码
class Sorter {
public:
virtual void sort(std::vector<int>& data) = 0;
};
class BubbleSort : public Sorter {
public:
void sort(std::vector<int>& data) override {
// Bubble Sort implementation
}
};
class QuickSort : public Sorter {
public:
void sort(std::vector<int>& data) override {
// Quick Sort implementation
}
};
class Context {
public:
void setStrategy(Sorter* strategy) {
this->strategy = strategy;
}
void executeSort(std::vector<int>& data) {
strategy->sort(data);
}
private:
Sorter* strategy;
};
2.4 命令 (Command)
命令模式将请求封装成对象,从而可以参数化方法调用。命令模式支持命令的异步执行、队列化以及撤销操作等功能。通过将命令对象传递给调用者和接收者,命令模式实现了请求发送者和接收者的解耦。
示例代码
class Command {
public:
virtual void execute() = 0;
};
class HelloWorldOutputCommand : public Command {
public:
void execute() override {
std::cout << "Hello, World!" << std::endl;
}
};
class Server {
public:
void acceptCommand(std::shared_ptr<Command> command) {
command->execute();
}
};
3 命令处理器 (Command Processor)
命令处理器模式进一步扩展了命令模式的应用,特别是在处理多个命令时。命令处理器可以管理和执行一系列命令,支持命令的组合、记录和重放功能。通过引入命令处理器,可以实现更复杂的命令处理逻辑,例如宏命令的执行。
示例代码
class CompositeCommand : public Command {
public:
void addCommand(std::shared_ptr<Command> command) {
commands.push_back(command);
}
void execute() override {
for (const auto& command : commands) {
command->execute();
}
}
private:
std::vector<std::shared_ptr<Command>> commands;
};
命令处理器模式的应用场景非常广泛,例如在图形界面应用程序中,用户可以执行一系列绘图操作,并且可以通过命令处理器撤销这些操作。
在接下来的部分,我们将详细介绍PIMPL习语及其在实际项目中的应用。PIMPL(Pointer to Implementation)是一种用于隐藏类实现细节的技术,有助于减少编译依赖并提高编译速度。通过将类的私有成员移到单独的实现类中,PIMPL可以帮助优化大型项目的编译时间和依赖管理。
4 PIMPL习语及其应用
PIMPL(Pointer to Implementation)是一种用于隐藏类实现细节的技术,有助于减少编译依赖并提高编译速度。通过将类的私有成员移到单独的实现类中,PIMPL可以帮助优化大型项目的编译时间和依赖管理。PIMPL也被称为Handle Body、Compilation Firewall或Cheshire Cat technique。这种方法使得类的接口部分保持稳定,即使内部实现发生更改,也不会影响使用该类的其他代码。
4.1 PIMPL的实现原理
PIMPL的主要思想是将类的私有成员变量和方法移到一个独立的实现类中,并通过一个指针(通常是智能指针)来间接访问这些成员。这样,头文件中只需要声明接口部分,而具体的实现则放在源文件中。当类的实现发生变化时,只有实现文件需要重新编译,而使用该类的其他代码不受影响。
示例代码
// Customer.h
#ifndef CUSTOMER_H_
#define CUSTOMER_H_
#include <memory>
#include <string>
class Address;
class Customer {
public:
Customer();
virtual ~Customer();
std::string getFullName() const;
void setShippingAddress(const Address& address);
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
#endif // CUSTOMER_H_
// Customer.cpp
#include "Customer.h"
#include "Address.h"
struct Customer::Impl {
int customerId;
std::string forename;
std::string surname;
Address shippingAddress;
};
Customer::Customer() : pImpl(std::make_unique<Impl>()) {}
Customer::~Customer() = default;
std::string Customer::getFullName() const {
return pImpl->forename + " " + pImpl->surname;
}
void Customer::setShippingAddress(const Address& address) {
pImpl->shippingAddress = address;
}
4.2 PIMPL的优点
- 减少编译依赖 :通过将实现细节隐藏在源文件中,减少了头文件之间的依赖关系,从而降低了编译时间。
- 提高编译速度 :当类的实现发生变化时,只有实现文件需要重新编译,而使用该类的其他代码不受影响。
- 保护实现细节 :PIMPL使得类的内部实现对使用者不可见,增加了代码的安全性和可维护性。
4.3 PIMPL的缺点
- 增加内存开销 :由于引入了额外的指针,可能会导致一定的内存开销。
- 性能影响 :每次访问私有成员时都需要通过指针间接访问,可能会带来轻微的性能损失。
4.4 PIMPL的实际应用
PIMPL在大型项目中非常有用,尤其是在频繁修改类的内部实现时。例如,在一个电商系统中,
Customer
类可能是核心业务实体,被多个模块引用。通过使用PIMPL,可以确保
Customer
类的接口保持稳定,即使内部实现发生变化,也不会影响其他模块的编译和运行。
5 总结与展望
5.1 总结
本文详细介绍了几种常用的设计模式及其应用场景,包括依赖注入、适配器、策略、命令和命令处理器模式。每种模式都有其独特的应用场景和实现方式,能够帮助开发者解决特定的设计问题。此外,还深入探讨了PIMPL习语,这是一种用于隐藏类实现细节的技术,有助于减少编译依赖并提高编译速度。
5.2 未来发展方向
随着软件开发的不断发展,设计模式和惯用法也在不断演进。未来,我们可以期待更多创新的设计模式和惯用法出现,以应对日益复杂和多样化的软件需求。同时,随着新技术的引入,现有的设计模式和惯用法也将不断优化和完善。
6 实际项目中的应用案例
6.1 案例1:电商系统中的依赖注入
在一个电商系统中,依赖注入被广泛应用于各个模块中,以解耦组件之间的依赖关系。例如,
OrderService
类依赖于
PaymentGateway
类来处理支付逻辑。通过依赖注入,
OrderService
类不需要直接创建或查找
PaymentGateway
实例,而是通过构造函数或setter方法接收依赖项。
应用流程
-
创建
PaymentGateway接口。 -
实现具体的支付网关,如
StripePaymentGateway和PayPalPaymentGateway。 -
在
OrderService类中,通过构造函数注入PaymentGateway实例。 - 使用依赖注入框架(如Spring或DI容器)管理依赖项的创建和注入。
6.2 案例2:图形界面中的命令模式
在图形界面应用程序中,命令模式被用于封装用户的操作请求。例如,用户可以绘制圆形、矩形等形状,并且可以通过命令处理器撤销这些操作。命令模式使得这些操作可以被参数化、队列化和撤销,从而提高了用户体验和系统的灵活性。
应用流程
-
定义命令接口
Command。 -
实现具体的命令类,如
DrawCircleCommand和DrawRectangleCommand。 -
创建命令处理器
CommandProcessor,用于管理和执行命令。 - 用户执行绘图操作时,创建相应的命令对象并传递给命令处理器。
- 通过命令处理器执行命令,并支持撤销功能。
7 表格总结
| 设计模式/惯用法 | 主要用途 | 优点 | 缺点 |
|---|---|---|---|
| 依赖注入 | 解耦组件依赖关系 | 提高代码模块化和可测试性 | 可能增加配置复杂度 |
| 适配器 | 使不兼容的接口协同工作 | 提高模块间的兼容性 | 可能增加代码复杂度 |
| 策略 | 允许算法独立变化 | 提高代码灵活性和可扩展性 | 可能增加类的数量 |
| 命令 | 封装请求为对象 | 支持异步执行和撤销操作 | 可能增加类的数量 |
| PIMPL | 隐藏实现细节 | 减少编译依赖,提高编译速度 | 增加内存开销和性能影响 |
8 流程图说明
graph TD;
A[依赖注入] --> B[构造函数注入];
A --> C[Setter注入];
B --> D[组件在构造时被完全初始化];
C --> E[适用于运行时动态注入依赖项的情况];
graph TD;
A[PIMPL] --> B[将类的私有成员移到单独的实现类中];
A --> C[通过智能指针间接访问私有成员];
B --> D[减少编译依赖];
B --> E[提高编译速度];
C --> F[保护实现细节];
通过以上内容,我们不仅深入了解了几种常用的设计模式及其应用场景,还掌握了PIMPL习语的具体实现和应用。希望这些知识能够帮助你在实际项目中构建高质量的软件系统。
超级会员免费看
10

被折叠的 条评论
为什么被折叠?



