设计原则和设计模式
目录
一、设计原则:设计模式的指导思想
二、从原则到模式:模板方法的演变过程
2.1 问题场景:动物园表演的 "重复与变化"
2.2 第一步:提取不变骨架(符合单一职责)
2.3 第二步:用抽象约束变化(符合依赖倒置)
2.4 第三步:固定流程顺序(符合开闭原则)
三、模板方法类的代码结构
四、关于多态组合以及普通组合
4.1 普通组合(Non-polymorphic Composition)
4.2 多态组合(Polymorphic Composition)
在程序员设计程序的时候,发现许多应用场景有类似的套路,同时为了代码的健壮性、可移植性等,经过许多人的总结,形成了设计原则。而设计模式是在设计原则的基础上不断迭代,逐渐总结出一套符合设计原则的固定套路。
一、设计原则:设计模式的指导思想
设计模式的本质是设计原则的组合应用。在面向对象设计中,几个核心原则构成了所有模式的基础:
(1)开闭原则(Open-Closed Principle)
对扩展开放,对修改关闭。即通过扩展已有代码来适应变化,而非修改原有代码。(2)依赖倒置原则(Dependency Inversion Principle)
依赖抽象而非具体实现。高层模块不依赖低层模块,两者都依赖抽象。(3)单一职责原则(Single Responsibility Principle)
一个类只负责一个功能领域的职责,避免因需求变化导致类频繁修改。(4)里氏替换原则(Liskov Substitution Principle)
子类可以替换父类并保证程序逻辑不变,即子类需遵守父类的行为契约。
这些原则都很好理解,比如开闭原则强调的是要对用户提供扩展接口,即可以定义子类去继承父类,而子类可以对父类虚函数重写达到扩展的效果。重点在于扩展,而非修改原有代码。
而里氏替换原则说不仅仅子类可以重写父类的虚函数,而且不能只关心子类自己的逻辑,还要满足父类的条件。
这些原则看似抽象,但当我们面对 "重复代码多、变化点分散、扩展困难" 的问题时,它们会引导我们找到结构化的解决方案 —— 这就是设计模式的诞生逻辑。
二、从原则到模式:模板方法的演变过程
这里我们以模板方法类为例:
模板方法模式(Template Method Pattern)的核心场景是:多个子类实现的流程骨架相同,但部分步骤的具体实现不同。我们通过一个演化案例来理解其与设计原则的关联。
2.1 问题场景:动物园表演的 "重复与变化"
动物园有多种表演(狮子表演、海豚表演、猴子表演),所有表演的执行流程骨架完全一致:
- 开场(播放音乐、主持人介绍);
- 主要表演(狮子跳火圈、海豚顶球等,具体内容不同);
- 观众互动(狮子与观众合影、海豚握手等,具体方式不同);
- 结束(谢幕、清理场地)。
但如果为每个表演单独编写代码,会出现大量重复(开场和结束逻辑完全相同),且新增表演时需重复编写骨架代码(违反开闭原则)。
2.2 第一步:提取不变骨架(符合单一职责)
将所有表演的不变步骤(开场、结束)抽离到一个公共父类,让变化步骤(主要表演、互动)留待子类实现。此时父类专注于 "流程骨架",子类专注于 "具体步骤",符合单一职责原则。
2.3 第二步:用抽象约束变化(符合依赖倒置)
父类通过纯虚函数定义变化步骤的接口,子类必须实现这些接口。高层模块(如调度表演的代码)只需依赖父类抽象,无需关心具体表演类型,符合依赖倒置原则。
2.4 第三步:固定流程顺序(符合开闭原则)
父类提供一个模板方法(Template Method),该方法按固定顺序调用所有步骤(包括不变步骤和抽象步骤)。子类通过重写抽象步骤扩展功能,无需修改模板方法本身,符合开闭原则。
至此,模板方法模式的核心结构已形成:父类定义流程骨架,子类实现变化步骤。
三、模板方法类的代码结构
// 抽象表演类:定义流程骨架(模板方法)
class Performance {
public:// 模板方法:固定表演流程,子类不可修改void run() {open(); // 不变步骤:开场perform(); // 变化步骤:主要表演(子类实现)interact(); // 变化步骤:观众互动(子类实现)close(); // 不变步骤:结束}// 析构函数声明为虚函数,确保子类析构正常调用virtual ~Performance() = default;protected:// 不变步骤:所有表演的开场逻辑相同void open() {std::cout << "=== 表演开始:播放背景音乐,主持人登场 ===" << std::endl;}// 不变步骤:所有表演的结束逻辑相同void close() {std::cout << "=== 表演结束:演员谢幕,工作人员清理场地 ===" << std::endl;}// 变化步骤:声明为纯虚函数,由子类实现具体表演内容virtual void perform() = 0;// 变化步骤:声明为纯虚函数,由子类实现具体互动方式virtual void interact() = 0;
};// 狮子表演:实现具体步骤
class LionPerformance : public Performance {
protected:void perform() override {std::cout << "狮子表演:跳火圈、钻铁笼" << std::endl;}void interact() override {std::cout << "互动环节:观众与狮子合影(保持安全距离)" << std::endl;}
};// 海豚表演:实现具体步骤
class DolphinPerformance : public Performance {
protected:void perform() override {std::cout << "海豚表演:顶球、跃出水面" << std::endl;}void interact() override {std::cout << "互动环节:观众给海豚喂鱼" << std::endl;}
};// 客户端代码:调度表演(依赖抽象,不依赖具体)
void startShow(Performance* performance) {performance->run(); // 调用模板方法,执行固定流程
}
代码解析:
抽象类
Performance
:
- 定义模板方法
run()
,固定调用open()
→perform()
→interact()
→close()
的顺序(骨架不变)。open()
和close()
为具体方法(不变步骤),perform()
和interact()
为纯虚函数(变化步骤,由子类实现)。子类实现:
LionPerformance
和DolphinPerformance
分别重写perform()
和interact()
,实现各自的具体逻辑,无需关心流程顺序。
假设你现在想要设定表演为狮子表演,只需要写一个狮子表演类,继承于Performance类,然后new一个狮子表演类对象A,直接用这个对象A的指针或者引用调用入口函数即可。
由于run在基类中已经确定了函数执行的顺序和步骤,所以A指针会在这个对象的虚函数表指针中找到重载的虚函数进行调用。
四、关于多态组合以及普通组合
组合(Composition)是 "has-a" 的关系(A 类包含 B 类的实例),是面向对象中比继承更灵活的复用方式。因为组合相比继承,可以降低代码的耦合度,如。且组合式黑箱复用,只需要调用类的接口即可,而继承则是白箱复用,对于子类而言可以看到父类的实现细节等。根据是否利用多态,组合可分为两类:
4.1 普通组合(Non-polymorphic Composition)
定义:组合的成员是具体类型的实例,编译时确定依赖关系。
class MusicPlayer
{
private:MP3Player mp3; // 依赖具体类型MP3Player
public:void play() { mp3.play(); }
};
特点:
- 编译时绑定(早绑定),性能略高,但灵活性差。
- 若需替换
MP3Player
为WavPlayer
,必须修改MusicPlayer
的代码(违反开闭原则)。
4.2 多态组合(Polymorphic Composition)
定义:组合的成员是抽象基类的指针 / 引用,运行时可指向任意派生类实例,依赖关系在运行时确定。
class Player { // 抽象基类
public:virtual void play() = 0;virtual ~Player() = default;
};class MP3Player : public Player {
public:void play() override { /* 播放MP3 */ }
};class WavPlayer : public Player {
public:void play() override { /* 播放Wav */ }
};class MusicPlayer {
private:Player* player; // 依赖抽象,运行时可指向MP3/WavPlayer
public:MusicPlayer(Player* p) : player(p) {}void play() { player->play(); }
};
特点:
- 运行时绑定(晚绑定),灵活性高,支持动态替换组件(符合开闭原则)。
- 高层模块(
MusicPlayer
)依赖抽象(Player
),符合依赖倒置原则。