设计模式的核心 - 隔离程序的变化点和稳定点
零:面向对象设计八大原则
①:依赖倒置(Dependency Inversion Principle)
-
高层模块不应依赖于低层模块,二者都应依赖于抽象:这意味着程序的高层逻辑不应该直接依赖于具体实现,而是应该依赖于抽象(如接口或抽象类)。
-
抽象不应依赖于细节,细节应依赖于抽象:这个原则强调了抽象的独立性和重要性,具体实现应该围绕抽象进行,而不是相反。
通过遵循DIP,可以减少模块之间的耦合,提高代码的可重用性和可维护性,促进更灵活的系统架构。
②:开放封闭原则”(Open/Closed Principle, OCP)
-
软件实体(类、模块、函数等)应该对扩展开放,对修改封闭:这意味着你应该能够通过增加新代码来扩展现有的功能,而不是修改已有的代码。通过这样做,可以避免引入错误,并减少对现有功能的影响。
-
通过抽象和继承来实现扩展:开放封闭原则通常通过使用接口或抽象类来实现。通过定义通用的接口或基类,可以在不修改原有代码的情况下,创建新的实现类。
-
示例:比如,在一个绘图程序中,假设有一个
Shape
接口和多个实现类如Circle
和Square
。如果需要添加一个新的形状Triangle
,可以通过创建Triangle
类来实现,而无需修改Shape
接口或其他已存在的类。这种方式不仅符合开放封闭原则,还能提高代码的清晰度和可维护性。
实践中的好处
- 减少缺陷:由于不需要修改现有代码,因此可以降低引入错误的风险。
- 提高可维护性:新功能可以独立于现有代码进行开发和测试。
- 促进重用:由于遵循抽象原则,可以更容易地重用现有的模块。
③:单一职责原则(Single Responsibility Principle, SRP)
单一职责原则的核心内容:
-
每个类应有且仅有一个原因引起变化:这意味着一个类应该只承担一个职责或功能。如果一个类承担了多个职责,当其中一个职责发生变化时,可能会影响到其他职责,导致代码的耦合性增加和维护难度加大。
-
减少耦合:将不同的功能分开,可以降低模块之间的依赖关系,使得每个模块都能独立开发和测试。
-
提高可维护性:遵循单一职责原则的代码更易于理解和维护。如果一个类只处理单一任务,开发者可以更快地理解其功能,并在需要修改或扩展时,更少地影响其他部分。
实践中的好处
- 提高代码的可读性:每个类或模块的功能清晰明了,易于理解。
- 简化测试:单一职责的类更易于单元测试,因为每个类都只包含特定的功能。
- 增强灵活性:在添加新功能时,可以更方便地引入新类,而不需要修改现有的类。
④:里氏替换原则(Liskov Substitution Principle, LSP)
里氏替换原则的关键点:
-
子类应该可以替代父类:任何可以使用父类的地方,都应该能够使用其子类,而不会改变程序的预期行为。这要求子类在行为上要与父类一致。
-
方法的预期行为:子类应该遵循父类的约定,包括输入输出、异常处理等。子类在实现父类的方法时,应该保持父类方法的语义,不能改变方法的预期效果。
-
不应改变父类的行为:子类不能对父类的功能进行不必要的修改,比如改变方法的返回值类型或抛出不同的异常。
实践中的好处
- 增强代码的可重用性:遵循 LSP 可以使子类和父类之间的关系更加清晰,从而提高代码的可重用性。
- 提高系统的灵活性和可扩展性:当新功能需要添加时,可以通过添加新的子类来实现,而不需要修改已有的代码。
- 降低错误风险:确保子类能够正确替代父类,降低了因不当替换而导致的错误风险。
⑤:接口隔离原则(Interface Segregation Principle, ISP)
接口隔离原则的关键点:
-
细化接口:将大接口分解成多个小接口,使每个接口只包含客户端所需的方法。这有助于减少客户端对不必要功能的依赖。
-
降低耦合:通过引入多个小接口,可以降低模块之间的耦合度,使得系统的各个部分更加独立,有助于提升系统的灵活性和可维护性。
-
提高可替换性:小接口的实现可以更容易被替换或修改,因为客户端只依赖于自己所需的接口,不会影响到其他模块。
实践中的好处
- 增强灵活性:通过接口隔离,系统中的每个部分可以独立修改和扩展,而不影响其他部分。
- 提高可维护性:每个接口都只包含必要的方法,使得代码更易于理解和维护。
- 减少不必要的依赖:客户端只需依赖于自己关心的接口,从而降低了不必要的耦合。
⑥:优先使用对象组合而不是类继承(Favor Composition Over Inheritance)
关键概念
-
对象组合:通过将不同的对象组合在一起以实现复杂的功能。每个对象可以独立地负责其特定的功能,多个对象可以协作完成更复杂的任务。
-
类继承:通过继承父类来获得其属性和方法。子类会直接获得父类的所有功能,同时也可能会因为父类的变化而受到影响。
为什么选择组合?
-
减少耦合:组合允许对象之间的关系更加灵活,改变或替换一个对象通常不会影响其他对象。相比之下,继承会导致较强的耦合关系,父类的变化可能会影响所有子类。
-
提高灵活性:组合可以在运行时动态地改变对象的行为,而继承在编译时就已经确定,灵活性较低。通过组合,系统可以在不改变现有代码的情况下添加新功能。
-
避免多重继承的问题:许多编程语言(如 Java 和 C#)不支持多重继承,而组合可以轻松实现类似的效果。通过组合不同的对象,可以获得多种功能而不产生复杂的继承层次结构。
-
增强可维护性:通过组合,代码变得更易于理解和维护。每个对象的职责明确,便于管理和修改。
实践中的好处
- 模块化设计:组合使得系统可以由小模块构建,便于重用和测试。
- 灵活性和扩展性:在需求变化时,可以通过增加新组件而不是修改现有类来快速响应变化。
- 清晰的责任分离:组合能够更好地体现单一责任原则(Single Responsibility Principle),使得每个对象都聚焦于自己特定的功能。
⑦:封装变化点(Encapsulate What Varies)
关键概念
-
变化点的识别:在设计软件时,需要识别出那些会随时间、需求或技术演进而变化的部分。这些变化点可以是算法、策略、配置、外部服务或数据格式等。
-
封装的方式:通过使用接口、抽象类、策略模式、工厂模式等设计模式来封装变化点。这些技术允许在不影响系统其他部分的情况下替换或修改变化点的实现。
-
降低耦合:将变化点与系统其他部分分开,从而降低各部分之间的耦合度,使得系统更加灵活和易于修改。
示例
假设我们正在设计一个处理订单的系统,系统需要根据不同的支付方式(如信用卡、PayPal、银行转账等)处理支付。如果直接在订单类中实现支付逻辑,未来添加新的支付方式时就会导致订单类变得复杂并且难以维护。
不封装变化点的设计:
class Order {
public:
void processPayment(const std::string& paymentType) {
if (paymentType == "CreditCard") {
// 处理信用卡支付
handleCreditCardPayment();
} else if (paymentType == "PayPal") {
// 处理 PayPal 支付
handlePayPalPayment();
} else if (paymentType == "BankTransfer") {
// 处理银行转账
handleBankTransferPayment();
} else {
std::cout << "Unsupported payment type." << std::endl;
}
}
};
在这个设计中,Order
类对支付方式的变化非常敏感。如果要添加新的支付方式,必须修改 Order
类的代码,这样会增加出错的机会并且降低代码的可维护性。
封装变化点的设计:
通过使用策略模式,我们可以将支付逻辑封装在独立的支付类中:
#include <iostream>
#include <memory>
// 支付策略接口
class PaymentStrategy {
public:
virtual void pay(double amount) = 0; // 纯虚函数,表示支付
virtual ~PaymentStrategy() {} // 虚析构函数
};
// 信用卡支付实现
class CreditCardPayment : public PaymentStrategy {
public:
void pay(double amount) override {
std::cout << "Processing credit card payment of $" << amount << std::endl;
}
};
// PayPal支付实现
class PayPalPayment : public PaymentStrategy {
public:
void pay(double amount) override {
std::cout << "Processing PayPal payment of $" << amount << std::endl;
}
};
// 银行转账支付实现
class BankTransferPayment : public PaymentStrategy {
public:
void pay(double amount) override {
std::cout << "Processing bank transfer payment of $" << amount << std::endl;
}
};
// 订单类
class Order {
private:
std::shared_ptr<PaymentStrategy> paymentStrategy; // 使用 shared_ptr 管理支付策略
public:
// 构造函数接收一个 shared_ptr
Order(std::shared_ptr<PaymentStrategy> strategy)
: paymentStrategy(strategy) {}
void processPayment(double amount) {
paymentStrategy->pay(amount); // 使用支付策略进行支付
}
};
优势
-
灵活性:新的支付方式可以通过实现
PaymentStrategy
接口轻松添加,而无需修改Order
类的代码。 -
可维护性:系统的不同部分之间的耦合度降低,使得每个部分可以独立修改和测试。
-
可读性:通过将变化的部分抽象出来,代码的结构更加清晰,易于理解。
实践中的应用
封装变化点的原则不仅适用于支付处理的例子,还广泛适用于其他场景,如:
- 用户界面:不同的UI实现(如Web、移动端)可以通过界面抽象化来处理。
- 数据存储:将数据存取的具体实现封装到仓储模式中,使得数据源的变化不会影响到业务逻辑。
- 算法:通过策略模式封装算法实现,使得算法的变化不会影响到其他逻辑。
⑧:针对接口编程,而不是针对实现编程
定义
-
针对接口编程(Programming to an Interface):
- 在代码设计中,程序员依赖于抽象接口(如接口或抽象类)来定义系统的行为。这意味着代码只需要知道如何使用接口,而不关心具体的实现细节。
- 通过使用接口,程序可以在运行时根据需要选择不同的实现,这种方式提供了更大的灵活性。
-
针对实现编程(Programming to an Implementation):
- 这种方法是指代码直接依赖于具体的类和实现,而不是抽象接口。这种方式会导致代码变得紧耦合,不易于修改和扩展。
- 如果实现发生变化,可能需要修改依赖于该实现的所有代码,这会增加维护成本。
一:Template method模式
特点:晚绑定:"我来调用你,而不是你来调用我"
模板方法模式主要由以下几个角色组成:
-
抽象类(Abstract Class):
- 包含一个或多个模板方法。
- 在模板方法中定义算法的基本步骤,部分步骤为抽象方法,子类需要实现这些方法。
-
具体类(Concrete Class):
- 继承抽象类并实现抽象方法。
- 具体类可以修改某些步骤的实现,从而实现算法的具体细节。
2. 模式的优点
- 代码复用:模板方法允许在多个子类中复用相同的算法结构。
- 控制反转:子类可以选择实现部分算法步骤,从而灵活地变化算法行为。
- 易于扩展:添加新算法只需创建新的子类,无需修改已有代码。
3. 模式的缺点
- 灵活性下降:由于算法的步骤是固定的,灵活性可能不如策略模式。
- 类数增加:每个不同的算法都需要一个新的子类,可能导致类的数量增加。
代码实践
【设计模式专题之模板方法模式】18-咖啡馆
#include<iostream>
#include<vector>
#include<memory>
using namespace std;
#define endl '\n'
class Coffee {
public:
void Run()
{
Name_coffee();
Grind();
Brewing();
Add_condiments();
}
protected:
Coffee() {};
void Grind()
{
cout << "Grinding coffee beans" << endl;
}
void Brewing()
{
cout << "Brewing coffee" << endl;
}
virtual void Name_coffee() = 0;
virtual void Add_condiments() = 0;
virtual ~Coffee() = default;
};
class Latte : public Coffee {
protected:
void Name_coffee() override
{
cout << "Making Latte:" << endl;
}
void Add_condiments() override
{
cout << "Adding milk" << endl << "Adding condiments" << endl;
}
};
class A_coffee : public Coffee {
protected:
void Name_coffee() override
{
cout << "Making American Coffee:" << endl;
}
void Add_condiments() override
{
cout << "Adding condiments" << endl;
}
};
signed main() {
cin.tie(0) -> sync_with_stdio(false);
int x = 0;
while (cin >> x)
{
shared_ptr<Coffee> coffeePtr;
switch (x)
{
case 1:
coffeePtr = make_shared<A_coffee>();
break;
case 2:
coffeePtr = make_shared<Latte>();
break;
default:
continue;
}
coffeePtr -> Run();
cout << endl;
}
return 0;
}
二:策略模式
1:关键组成部分
- 策略接口:定义所有支持的算法的公共接口。
- 具体策略类:实现策略接口,封装具体的算法。
- 上下文(Context):持有一个对策略接口的引用,可以在运行时切换策略。
2:工作原理
- 上下文类使用策略接口来调用具体的策略算法。
- 客户端可以根据需要选择并设置具体的策略,策略的实现细节对客户端是透明的。
3:适用场景
- 当你有多个相关的类,仅仅是行为不同,而可以通过不同的算法实现。
- 当你需要在运行时选择算法。
- 当你希望避免使用大量的条件语句(如
if-else
或switch
)。
代码实践:
【设计模式专题之策略模式】14. 超市打折
分析:该问题中,策略执行往往是稳定的,变化点在于用户策略的选择。
#include<iostream>
#include<algorithm>
#include<vector>
#include<memory>
using namespace std;
#define endl '\n'
const int maxn = 1e6 + 10;
int n, m, k, d, T = 1, A, B;
//支付基类
class Pay {
public:
Pay() {}
virtual ~Pay() = default;
virtual int cost(int x) const = 0;
};
class Percentage_pay : public Pay {
public:
int cost(int amount) const override
{
return amount * 0.9;
}
};
class Full_pay : public Pay {
public:
int cost(int amount) const override
{
if(amount >= 300)return amount - 40;
if(amount >= 200)return amount - 25;
if(amount >= 150)return amount - 15;
if(amount >= 100)return amount - 5;
return amount;
}
};
//具体策略类
class Shopping {
private:
std::shared_ptr<Pay> ptr;
public:
Shopping(std::shared_ptr<Pay> p) : ptr(p) { }
void Run(int x) const
{
cout << ptr -> cost(x) << endl;
}
};
signed main()
{
cin.tie(0) -> sync_with_stdio(false);
int T = 0;
cin >> T;
std::shared_ptr<Pay> ptr;
while(T--)
{
cin >> n >> m;
if(1 == m)ptr = make_shared<Percentage_pay>();
else if(2 == m) ptr = make_shared<Full_pay>();
Shopping shop(ptr);
shop.Run(n);
}
return 0;
}