**"组件协作"模式:**现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式是通过晚期绑定,来实现框架和应用程序之间的松耦合,是二者之间协作常用的模式。
Template Method
动机
在软件构建过程中,对于某一项任务,它常常有稳定 的整体操作结构,但各个子步骤去有很多改变 的需求,或者由于固有原因(比如框架和应用之间的关系)而无法和任务的整体结构同时实现。
如果在确定稳定操作结构 的前提下,来灵活应对各个子步骤的变化或晚期实现需求 ?
定义
Define the skeleton of an algorithm in an operation(稳定), deferring(变化) some steps to subclasses. Template Method lets subclass redefine(重写 override) certain steps of an algorithm without changing(复用) the algorithm’s structure.
实现
结构化软件设计流程
template1_lib.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Library { public : void Step1 () { } void Step3 () { } void Step5 () { } };
template_app.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Application { public : bool Step2 () { } void Step4 () { } }; int main () { Library lib () ; Application app () ; lib.Step1 (); if (app.Step2 ()) { lib.Step3 (); } for (int i = 0 ; i < 4 ; i++) { app.Step4 (); } lib.Step5 (); }
应用程序开发人员需要控制程序主流程。
如果程序主流程是稳定,那么放到程序库中是更合适的。
但是程序库开发早,应用程序开发晚,怎么实现“晚绑定 ”呢?—— 虚函数 / 函数指针。
Template Method 面向对象软件设计流程
template2_lib.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Library { public : 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 ; };
template2_app.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Application : public Library { protected : virtual bool Step2 () { } virtual void Step4 () { } }; int main () { Library* pLib = new Application (); lib->Run (); delete pLib; }
通过 Template Method,变成了程序库开发人员控制程序主流程。
C 语言严格上也有晚绑定机制,只是用的是函数指针(抽象性比如虚函数,但是虚函数本质上也是虚函数表上挂了函数指针)。
扩展:如果没有稳定的、可复用的算法骨架,那么假设就不成立,不能使用 Template Method。
结构
总结
Template Method 模式是一个非常基础性的设计模式,在面向对象系统中有大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用你”的反向控制是 Template Method 的典型应用。
在具体实现方面,被 Template Method 调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为 protected
方法。
为啥要设为 protected
?
这些方法往往是要放到一个流程中才有意义,单独作为一个 public
接口让用户去使用意义不大,因此设置为 protected
。
Strategy
Strategy
也称为 Policy
。
动机
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
如何在运行时根据需要透明地更改对象的算法?将算法与对象本体解耦,从而避免上述问题?
定义
Define a family of algorithm, encapsulate each one, and make them interchangeable(变化). Strategy lets the algorithm vary independently from clients(稳定) that use it(扩展,子类化).
实现
Naive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 enum TaxBase { CN_Tax, US_Tax, DE_Tax, FR_Tax }; class SalesOrder { TaxBase tax; public : double CalculateTax () { if (tax == CN_Tax) { } else if (tax == US_Tax) { } else if (tax == DE_Tax) { } else if (tax == FR_Tax) { } } };
这段代码初看没啥问题,但作为程序员,不能静态的看问题,要有时间轴观念——考虑未来的变化 。
新的需求将导致:重新修改、重新编译、重新测试、重新部署。
违反“开放封闭原则”:应该对扩展开放,对修改封闭。
Strategy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 class TaxStrategy { public : virtual double Calculate (const Context& context) = 0 ; virtual ~TaxStrategy () {} }; 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 : SalesOrder (StrategyFactory* strategyFactory) { this ->strategy = strategyFactory->NewStrategy (); } ~SalesOrder () { delete this ->strategy; } public double CalculateTax () { Context context () ; double val = strategy->Calculate (context); } };
把之前的一个个算法,变成了这里的一个个 TaxStrategy 的子类。
工程实现应该把一个类放到一个文件中,这里为了方便表示,就不那样做了。
设计模式中“复用”指的是编译单位,二进制层面的复用性,是编译测试后原封不动;源码级别的复制粘贴不叫复用。
结构
Context 对应 SalesOrder
Strategy 对应 TaxStrategy
总结
Strategy 及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时 方便地根据需要在各个算法之间进行切换。
Strategy 模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要 Strategy 模式。
除非条件绝对稳定不变,才不需要考虑 Strategy,比如一周七天的判断。
如果 Strategy 对象没有实例变量,那么各个上下文可以共享同一个 Strategy 对象,从而节省对象开销。(不是特别重要)
Observer / Event
Observer
也被称为 Event
, Dependents
, Publish-Subscribe
。
动机
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
定义
Define a one-to-many(变化) dependency between objects so that when one object changes, all its dependents are notified and update automatically.
实现
Naive
FileSplitter1.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class FileSplitter { string m_filePath; int m_fileNumber; ProgressBar* m_progressBar; public : FileSplitter (const string& filePath, int fileNumber, ProgressBar* progressBar) : m_filePath (filePath), m_fileNumber (fileNumber), m_progressBar (progressBar) {} void split () { for (int i = 0 ; i < m_fileNumber; i++) { float progressValue = m_fileNumber; progressValue = (i + 1 ) / progressValue; m_progressBar->setValue (progressValue); } } };
MainForm1.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class MainForm : public Form { TextBox* txtFilePath; TextBox* txtFileNumber; ProgressBar* progressBar; public : void Button1_Click () { string filePath = txtFilePath->getText (); int number = atoi (txtFileNumber->getText ().c_str ()); FileSplitter splitter (filePath, number, progressBar) ; splitter.split (); } };
思考问题在哪?(违法哪条原则)
这里 MainForm 依赖具体的 progressBar 是违反依赖倒置原则的:实现细节应该依赖抽象(稳定)。
Observer
这里的代码需要结合下一节的结构图来看。
FileSplitter2.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class IProgress { public : virtual void DoProgress (float value) = 0 ; virtual ~IProgress () {} }; class FileSplitter { string m_filePath; int m_fileNumber; List<IProgress*> m_iprogressList; public : FileSplitter (const string& filePath, int fileNumber) : m_filePath (filePath), m_fileNumber (fileNumber) {} void split () { for (int i = 0 ; i < m_fileNumber; i++) { float progressValue = m_fileNumber; progressValue = (i + 1 ) / progressValue; onProgress (progressValue); } } void addIProgress (IProgress* iprogress) { m_iprogressList.push_back (iprogress); } void removeIProgress (IProgress* iprogress) { m_iprogressList.remove (iprogress); } protected : virtual void onProgress (float value) { List<IProgress*>::iterator itor = m_iprogressList.begin (); while (itor != m_iprogressList.end ()) { (*itor)->DoProgress (value); itor++; } } };
MainForm2.cpp
形式上,把 setValue
通过 DoProgress
从 FilerSplitter搬到了 MainForm2。但实际上变化更大,这将不良耦合变成了良好的耦合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class MainForm : public Form, public IProgress { TextBox* txtFilePath; TextBox* txtFileNumber; ProgressBar* progressBar; public : void Button1_Click () { string filePath = txtFilePath->getText (); int number = atoi (txtFileNumber->getText ().c_str ()); ConsoleNotifier cn; FileSplitter splitter (filePath, number) ; splitter.addIProgress (this ); splitter.addIProgress (&cn); splitter.split (); splitter.removeIProgress (this ); } virtual void DoProgress (float value) { progressBar->setValue (value); } }; class ConsoleNotifier : public IProgress { public : virtual void DoProgress (float value) { cout << "." ; } };
结构
Observer 相当于 IProcess
,里面的 Update()
相当于 doProgress()
。
GOF 中推荐写一个基类,包含 Attach()
、Detach()
和 Notify()
,上述代码中把 Subject 和 ConcreteSubject合二为一了。这三个方法分别对应 addIProgress()
、 removeIProgress()
和 onProgress()
。
ConcreteObserver 相当于 MainForm 和 ConsoleNotifier。
总结
使用面向对象的抽象,Observer模式使得我们可以独立地 改变目标和观察者,从而使得二者之间的依赖关系达致松耦合。
可以添加多个具体的观察者,上面侯松的部分是不用变的。
目标发送通知时,无需指定观察者 ,通知(考研携带通知信息作为参数)会自动传播。
观察者自己决定是否需要订阅通知,目标对象对此一无所知。
Observer 模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式中的一个重要组成部分。