设计模式C++李建忠
一、设计模式的六大原则
总原则:开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。
在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。
1、单一职责原则
不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。
2、里氏替换原则(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。子类对象能够替换父类对象,而程序逻辑不变。
里氏替换原则有至少以下两种含义:
-
- 里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
-
- 如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。
- 如何符合LSP?总结一句话 —— 就是尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。
3、依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
5、迪米特法则(最少知道原则)(Demeter Principle)
就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。
6、合成复用原则(Composite Reuse Principle)
原则是尽量首先使用合成/聚合的方式,而不是使用继承。
二、GOF-23 设计模式的分类
2.1、从目的来看:
• 创建型(Creational)模式:将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象创建时具体类型实现引来的冲击。
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
• 结构型(Structural)模式:通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
• 行为型(Behavioral)模式:通过类继承或者对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
2.2、从范围来看:
- 类模式处理类与子类的静态关系。
- 对象模式处理对象间的动态关系。
2.3、从封装变化角度对模式分类:
组件协作:
Template Method
Observer/Event
Strategy
单一职责:
Decorator
Bridge
对象创建:
Factory Method
Abstract Factory
Prototype
Builder
对象性能:
Singleton
Flyweight(享元模式)
接口隔离:
Façade(门面模式)
Proxy
Mediator(中介者)
Adapter
状态变化:
Memento(备忘录)
State
数据结构:
Composite(组合模式)
Iterator
Chain of Resposibility(职责链)
行为变化:
Command
Visitor
领域问题:
Interpreter
现代较少用的模式
Builder
Mediator
Memento
Iterator
Chain of Resposibility
Command
Visitor
Interpreter
三、“组件协作”模式:框架与应用程序的划分
“组件协作”模式通过晚期绑定,来实现框架
与应用程序
之间的松耦合,是二者之间协作时常用的模式。
典型模式
• Template Method
• Observer / Event
• Strategy
3.1、Template methon 模板方法
模式定义:
定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟(变化)到子类中。
Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(override 重写)该算法的某些特定步骤。
——《设计模式》GoF
Q:出现了override,难道不违背LSP原则?
A:为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。
3.1.1、面向过程的代码结构:
在C语言编程中,常见的是先由Lib开发人员写好Library的代码,再由App开发人员去编写main(),然后再在main中调用Lib中的函数。
//Library.cpp-程序库开发人员
class Library{
public:
void Step1(){
//...
}
void Step3(){
//...
}
void Step5(){
//...
}
};
//Application.cpp-应用程序开发人员
class Application{
public:
bool Step2(){
//...
}
void Step4(){
//...
}
};
//main.cpp
int main()
{
Library lib();
Application app();
lib.Step1();
if (app.Step2()){
lib.Step3();
}
for (int i = 0; i < 4; i++){
app.Step4();
}
lib.Step5();
}
3.1.2、面向对象的代码结构:
在C++的面向对象编程思想中,先由Lib开发人员编写Library的代码,以及示例程序的运行框架run(),再由App开发人员实现run()中所需的虚函数,然后再在main()中调用run就可以,省去了App开发人员构思程序运行框架的工作。
典型的特征:App继承自Library,如下例class Application : public Library {}
//Library.cpp-程序库开发人员
class Library{
public:
//稳定 template method
void Run(){
Step1();
if (Step2()) { //支持变化 ==> 虚函数的多态调用
Step3();
}
for (int i = 0; i < 4; i++){
Step4(); //支持变化 ==> 虚函数的多态调用
}
Step5();
}
virtual ~Library(){ }
protected:
void Step1() { //稳定
//.....
}
void Step3() {//稳定
//.....
}
void Step5() { //稳定
//.....
}
virtual bool Step2() = 0;//变化
virtual void Step4() =0; //变化
};
//Application .cpp-应用程序开发人员
class Application : public Library {
protected:
virtual bool Step2(){
//... 子类重写实现
}
virtual void Step4() {
//... 子类重写实现
}
};
int main()
{
Library* pLib=new Application();
lib->Run();
delete pLib;
}
}
为了符合LSP原则,父类Library是一个抽象类!!!!
3.1.3、要点总结
- Template Method模式使用的机制:虚函数的多态性。
- 除了可以灵活应对子步骤的变化外,不要调用我(Lib),让我来调用你(App),的反向控制结构是Template Method的典型应用。
- 在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为protected方法。
3.2、Strategy 策略模式
模式定义:
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。
该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。
——《设计模式》GoF
3.2.1、面向过程的代码结构:
现有多种货币,对应的税率计算时不一样的。对传统C变成来说,就是if-else处理。
优点:过程清晰明了
缺点:不利于拓展和修改。若要增加币种,就要在原来程序中增加else结构。
enum TaxBase {
CN_Tax,
US_Tax,
DE_Tax,
FR_Tax //更改
};
class SalesOrder{
TaxBase tax;
public:
double CalculateTax(){
//...
if (tax == CN_Tax){
//CN***********
}
else if (tax == US_Tax){
//US***********
}
else if (tax == DE_Tax){
//DE***********
}
else if (tax == FR_Tax){ //更改
//...
}
//....
}
};
3.2.2、面向对象的代码结构
把计算税率的方法封装起来,根据传入的币种类型,自动调用对应的方法。
缺点:程序框架复杂。
优点:便于拓展和修改。
// 税率计算方法的抽象父类-TaxStrategy.cpp
class TaxStrategy{
public:
virtual double Calculate(const Context& context)=0;
virtual ~TaxStrategy(){}
};
// CN税率计算方法-CNTax.cpp
class CNTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class USTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class DETax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
//扩展
//*********************************
class FRTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//.........
}
};
// 主框架
class SalesOrder{
private:
TaxStrategy* strategy;
public:
// 传入一个工厂类,new创建对应当策略对象
SalesOrder(StrategyFactory* strategyFactory){
this->strategy = strategyFactory->NewStrategy();
}
~SalesOrder(){
delete this->strategy;
}
public double CalculateTax(){
//...
Context context();
//
double val =
strategy->Calculate(context); //多态调用,这就是面向接口编程,父类指针->纯虚方法
//...
}
};
3.2.3、要点总结
- Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
- Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式。
- 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。
3.3、Observer / Event 观察者模式
模式定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,
所有依赖于它的对象都得到通知并自动更新。
——《设计模式》GoF
3.3.1、 观察者模式又叫做发布-订阅(Publish/Subscribe)模式:
观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
- 特点:一般由两个角色组成:发布者和订阅者(观察者)。观察者通常有一个回调,也可以没有。
- 应用场景:监听器、xml解析、监听器、日志收集、短信通知、邮件通知
Spring 中Observer 模式常用的地方是Listener 的实现。如ApplicationListener。 - 优点:
观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;
观察者模式在观察目标和观察者之间建立一个抽象的耦合;
观察者模式支持广播通信;
观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求。 - 缺点:
如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;
如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。 - 角色构成:
1)抽象主题角色
2)抽象观察者角色
3)具体主题角色
4)具体观察者角色
3.3.2、面向对象的程序框架:
在C++中,多继承会带来许多bug,尽量不要多继承。但是有一种情况例外:有一个主要父类,其他父类都是接口。例如下面的程序。
// 接口抽象类-IPublish.cpp
class IPublish
{
public:
virtual void registerSubscribe(ISubscribe iSubscribe);
virtual void removeSubscribe(ISubscribe iSubscribe);
virtual void notifySubscribe(String publishInfo);
}
// 接口抽象类-ISubscribe.cpp
class ISubscribe {
public:
virtual void update(String publishInfo);
}
// 下面是类的具体实现
// Publish.cpp-发布类,即被观察者
// 在C++中,多继承会带来许多bug,尽量不要多继承。但是有一种情况例外:有一个主要父类,其他父类都是接口。
class Publish: public Form, public IPublish
{
private:
List<ISubscribe> iSubscribes = new ISubscribe<List<ISubscribe>>();
// 把所有Subscribe保存在一个集合中,每个Publish角色都可以有任意数量的Subscribe。
// Publish提供一个接口,可以增加和删除Subscribe角色。一般用一个抽象类和接口来实现。
public:
void registerSubscribe(ISubscribe iSubscribe) {
this.iSubscribes.add(iSubscribe);
}
void removeSubscribe(ISubscribe iSubscribe) {
this.iSubscribes.remove(iSubscribe);
}
// 在Publish内部状态改变时,给所有登记过的Subscribe发出通知。
void notifySubscribe(String publishInfo) {
for(ISubscribe iSubscribe : iSubscribes){
iSubscribe.update(publishInfo);
}
}
}
// SubscribeA.cpp-订阅类,即观察者
class SubscribeA: public ISubscribe {
public:
void update(String publishInfo) {
System.out.println("订阅者A, 信息:" + publishInfo);
}
}
// Subscribeb.cpp-订阅类,即观察者
class SubscribeB: public ISubscribe {
public:
void update(String publishInfo) {
System.out.println("订阅者B,信息:" + publishInfo);
}
}
// Test.cpp-测试类
public class ObserverTest {
public:
static void main(String[] args) {
try {
IPublish publish = new Publish();
ISubscribe subscribeA = new SubscribeA();
ISubscribe subscribeB = new SubscribeB();
publish.registerSubscribe(subscribeA);
publish.registerSubscribe(subscribeB);
publish.notifySubscribe("test");
Thread.sleep(10000);
publish.notifySubscribe("designParrten test");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.3.3、要点小结:
- Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
- 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
- Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。