本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版 | |||
cs:programming:cpp:boolan_cpp:design_pattern_1 [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:boolan_cpp:design_pattern_1 [2024/01/14 13:46] (当前版本) – ↷ 页面programming:cpp:boolan_cpp:design_pattern_1被移动至cs:programming:cpp:boolan_cpp:design_pattern_1 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ====C++设计模式 第一周==== | ||
+ | 本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\ | ||
+ | <wrap em> | ||
+ | ---- | ||
+ | ====设计模式与面向对象==== | ||
+ | |||
+ | 人们在解决问题的过程中,常常会采取两种方式:**分而治之**和**抽象**。**分治**的核心概念是将大问题分解为一系列规模较小的问题,然后各个击破。而**抽象**则是针对问题本身的特点进行观察,最大程度的提取该问题和其他问题的共同性,以方便以后类似问题的解决。换而言之,抽象对于分治来说,更加强调**复用性**。抽象和复用性并不是单纯的存在于软件设计中,而是作为人类思考、进步的一种哲学指导思想存在的——人类最伟大的抽象工程之一就是数学定理和公式。由此可见,抽象和复用性在人类生活中占据着无与伦比的重要地位。这同样适应于软件设计:在软件工程中,一个主要衡量代码设计质量的指标,就是其本身的**复用性**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | C++ 中通过面向对象的中的继承和多态来体现抽象的思想FIXME(对象组合)。举例来说,如果希望设计一个画图类,那么无论是话什么图形,都可以通过继承一个通用的父类 '' | ||
+ | |||
+ | ====面向对象的设计原则==== | ||
+ | |||
+ | 上面谈到C++中面向对象是抽象思想赖以实现的原则。抽象的最大的特点是重用性,也就是它本身的优势:**抵御变化**。为了达到这个目的,我们需要明确一个概念:并不是所有的面向对象都可以称为抽象。只有在向对象设计中保证了各个功能模块各司其职,并相互不影响的情况下,抽象思想才可以实现。也就是说,最理想的情况下,每个模块的**责任**是不重复的;而我们对系统添加新的功能,只应该通过添加新的模块去实现。只有这样,才能最大限度的隔离变化,抵御变化带来的影响。为了做到这一点,面向对象设计中提出了一系列的基本原则作为设计的思想,来确保设计出来的系统具有抽象思想的特性。这些原则如下: | ||
+ | \\ | ||
+ | \\ | ||
+ | 1.**依赖倒置原则**(// | ||
+ | * 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定)。 | ||
+ | * 抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。 | ||
+ | 总的来说,就是不管自身是否稳定,所依赖的对象必须是稳定的,这样才能抵御“变化”。 | ||
+ | \\ | ||
+ | \\ | ||
+ | |||
+ | 2.**开放封闭原则**(O// | ||
+ | * // | ||
+ | *// 类模块应该是可扩展的,但是不可修改// | ||
+ | 也就是说,在程序需有新需求(变化)的时候,不能去修改原有的模块,而是添加新的模块来实现。换句话说,是为了使程序具有良好的扩展性,易于维护和升级。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 3.**单一职责原则**(// | ||
+ | * // | ||
+ | * // | ||
+ | 这一条原则在强调类的功能必须专一。如果一个类包含了过多的功能,也就意味着这个类需要去承担更多的责任;一旦发生变化,该类需要修改的几率大大的增加。而同时,包含过多的内容意味着类的逻辑将更加复杂,可读性会更低。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 4. **// | ||
+ | * // | ||
+ | * // | ||
+ | 里氏替换原则强调了什么是真正的复用(也就是继承的真正含义// | ||
+ | \\ | ||
+ | \\ | ||
+ | 5. 接口隔离原则(Interface Segregation Principle): | ||
+ | * // | ||
+ | * // | ||
+ | 该原则实际上在强调一点:使用多个隔离的接口,比使用单个接口要好。换句话说就是,我们需要尽量降低类之间的耦合度,以及相互之间的依赖度。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 6.**合成复用原则**(// | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | 使用复合代替继承作为类之间的关系可以有效的降低类之间的耦合度。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 7.**封装变化点** | ||
+ | * // | ||
+ | \\ | ||
+ | 8. **针对接口编程,而不是针对实现编程** | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | \\ | ||
+ | 以上的规则,实际上在强调抽象和重复性的重要性,以及保证这个重要性的前提:**模块化**。模块化不仅是软件设计成功的前提,在人类社会的其他行业中,具有模块化标准的产业,也同样是行业内的翘楚。 | ||
+ | |||
+ | ====设计模式的总览==== | ||
+ | |||
+ | 设计模式在 //GOF// 进行了详细的分类。如果按照**使用的目的**来分类,设计模式可以分为如下几种: | ||
+ | * 创建型(// | ||
+ | * 结构型(// | ||
+ | * 行为型(// | ||
+ | 如果按照**范围**来看则可以分为两种: | ||
+ | * 以类模式来处理类与子类的**静态**关系,通常针对继承来说。 | ||
+ | * 以对象模式来处理对象之间的**动态**关系,通常针对于组合关系来说。 | ||
+ | 不过在实际的应用中,可以将这些设计模式从变化的角度来进行分类: | ||
+ | \\ | ||
+ | \\ | ||
+ | <WRAP 100%> | ||
+ | ^ 组件协作 | ||
+ | | Template Method | ||
+ | | Strategy | ||
+ | | Observer / Event | | Prototype | ||
+ | | | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 相信这么多设计模式,估计你也跟我一样看花眼了吧m(。那么有没有一个正确的方法知道应该如何正确使用对应的设计模式呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | 答案是**重构获得模式**(// | ||
+ | \\ | ||
+ | \\ | ||
+ | 关于重构的书籍,有两本值得推荐:// | ||
+ | \\ | ||
+ | \\ | ||
+ | 除了理论上意外,还具有一些具体的技巧,比如: | ||
+ | * 静态绑定转换到动态绑定 | ||
+ | * 早绑定转换到晚绑定 | ||
+ | * 继承关系转为组合关系 | ||
+ | * 编译时依赖转换到运行时依赖 | ||
+ | * 紧耦合转换到松耦合 | ||
+ | |||
+ | ====设计模式:组件协作==== | ||
+ | |||
+ | 现代软件专业分工之后的第一个结果是“框架与应用程序的划分”。“组件协作”模式通过晚期绑定,来实现**框架**与**应用程序**之间的松耦合,是二者之间协作时常用的模式。属于组件协作模式的设计模式有三种:// | ||
+ | \\ | ||
+ | \\ | ||
+ | ===4.1.Template Method=== | ||
+ | \\ | ||
+ | //Template Method// 适用于软件构建过程中框架处于稳定状态,但细节或者子步骤(应用层)需要大量改变的需求的情况。这样的情况从具体的实现上来看是像下图这样的: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 如上图,框架开发者负责开发'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 按传统的结构化设计来看(左边),如果要实现一个指令序列,那么这个整体的命令序列是要通过应用开发者去完成的;但在我们先前提到的情况中,框架端是没有变化的(稳定的);也就是说,我们知道,整个软件的流程是不变的,唯一变化的只有应用开发者负责开发的部分。如果由应用层开发者来实现整个流程,那么有两个问题将会出现: | ||
+ | - 应用层开发者必须明白框架开发者的开发内容。 | ||
+ | - 框架开发者如果需要改变流程,必须与应用层开发者进行沟通。 | ||
+ | 上面这两个问题会大大的增加软件开发的成本和可维护性。我们不禁想到,有没有可能将整体流程部分直接交给框架开发者直接制定,使得应用开发者不用承担不必要的负担? | ||
+ | \\ | ||
+ | \\ | ||
+ | //Template Method// 正是应此而生的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 传统结构化设计,我们通过应用端对框架端进行调用,我们称之为**早绑定**(// | ||
+ | <code cpp linenums: | ||
+ | /* Library & Framework Dev */ | ||
+ | class Library { | ||
+ | public: | ||
+ | void Step1() { //... } | ||
+ | void Step3() { //... } | ||
+ | void Step5() { //... } | ||
+ | }; | ||
+ | |||
+ | /* Application */ | ||
+ | class Application { | ||
+ | public: | ||
+ | bool Step2(){ //... } | ||
+ | void Step4(){ //... } | ||
+ | }; | ||
+ | /* Main workflow */ | ||
+ | int main() | ||
+ | { | ||
+ | Library lib(); | ||
+ | Application app(); | ||
+ | lib.Step1(); | ||
+ | if (app.Step2()) { lib.Step3(); | ||
+ | for (int i = 0; i < 4; i++) { | ||
+ | app.Step4(); | ||
+ | } | ||
+ | lib.Step5(); | ||
+ | } | ||
+ | </ | ||
+ | 我们可以看到,因为整个程序的流程实现是交给应用层开发者的,因此流程是变化的,对于框架开发者来说也是不可控的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 而相比之下,如果将流程交由框架开发者来实现,也就是框架开发者在流程中反过来调用应用开发者创造的内容,我们称这种方法为**晚绑定**(// | ||
+ | <code cpp linenums: | ||
+ | /* Libary & FrameWork with template method */ | ||
+ | class Library { | ||
+ | public: | ||
+ | // | ||
+ | void Run() { | ||
+ | Step1(); | ||
+ | //variation -> virtual funtion call (step 2) | ||
+ | if (Step2()) { Step3(); } | ||
+ | for (int i = 0; i < 4; i++){ | ||
+ | Step4(); //variation -> virtual funtion call (step 4) | ||
+ | } | ||
+ | Step5(); | ||
+ | } | ||
+ | virtual ~Library(){ } | ||
+ | protected: | ||
+ | void Step1() {.....} | ||
+ | void Step3() {.....} | ||
+ | void Step5() {.....} | ||
+ | virtual bool Step2() = 0; //variation | ||
+ | virtual void Step4() = 0; //variation | ||
+ | }; | ||
+ | /* Application */ | ||
+ | class Application : public Library { | ||
+ | protected: | ||
+ | virtual bool Step2(){...} | ||
+ | virtual void Step4() {...} | ||
+ | }; | ||
+ | int main() | ||
+ | { | ||
+ | Library* pLib=new Application(); | ||
+ | lib-> | ||
+ | delete pLib; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | 框架开发者将 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 明白了这些,我们来看一看 //Template Method// 的具体定义吧: | ||
+ | \\ | ||
+ | \\ | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | 一个稳定的框架,一个稳定的算法(工作流程),加上一些需求有变化的应用,通过 //Template Method// ,就可以让应用层的内容去复用这个算法(流程)了。前面三者是条件,最后一个是结果。这就是// | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Template Method// 是一种非常基础性的设计模式,在面向对象的系统中,有着大量的应用。他用最简洁的机制(虚函数的多态性)为很多应用程序的框架提供了灵活的扩展点,是代码复用方面的基本实现结构。 | ||
+ | * 除了可以灵活对应子步骤的变化外,“不要调用我,让我来调用你”的反向控制结构是 //Template Method// 的典型应用。 | ||
+ | * 在具体实现方面,被 //Template Method// 调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),一般推荐将他们设置为 // | ||
+ | |||
+ | ===Strategy=== | ||
+ | |||
+ | 在软件设计的过程中,某些对象使用的**算法会经常发生改变**。如果将这些算法改变的所有可能性都集成到对象中,那么可以想象到的是,对象会变得异常臃肿;并且,一些对象根本不会用到的算法,也会占用资源,造成性能负担。这种情况下,我们就需要考虑使用 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们来看一个具体的例子:我们需要设计一个计算税的系统,该系统需要支持不同国家的税率算法。按照结构化程序的思路,我们会专门为该系统设计一个总的 '' | ||
+ | <code cpp linenums: | ||
+ | enum TaxBase { | ||
+ | CN_Tax, | ||
+ | US_Tax, | ||
+ | DE_Tax, | ||
+ | }; | ||
+ | /* Tax Algorithm */ | ||
+ | class SalesOrder { | ||
+ | TaxBase tax; | ||
+ | public: | ||
+ | double CalculateTax() { | ||
+ | if (tax == CN_Tax) { .... } | ||
+ | else if (tax == US_Tax) { .... } | ||
+ | else if (tax == DE_Tax) { .... } | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 这种方法看上去并没有什么太大的问题。但如果我们以是否具有拓展性的标准(也就是带着时间轴去看)去看待这种实现方法,我们会发现一些问题。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 假设我们需要该系统增加对新的国家的税率计算支持,如果按照上面的实现方法,我们需要修改所有的类: | ||
+ | * 添加对应的判断条件到 '' | ||
+ | * 添加对应的税率算法到 '' | ||
+ | 只有完善了这些新的信息,我们的系统才能具有对新国家的税率的计算能力。然而,根据面向对象的设计原则,我们这里修改了好几个类(模块) 的功能。也就是说,我们没有在不修改原有模块的基础上进行拓展;在这一点上我们违反了**开放封闭原则**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 而从另一个角度上看,添加的这些算法,在一定程度上造成了对象的臃肿。原来的每个对象需要搭载 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此,对于这样的情况,我们可以采用如下的设计方法,也就是 // | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 从上图可以明确的看出, // | ||
+ | \\ | ||
+ | \\ | ||
+ | 具体的代码如下: | ||
+ | <code cpp linenums: | ||
+ | /* Tax Algorithm base class */ | ||
+ | class TaxStrategy { | ||
+ | public: | ||
+ | virtual double Calculate(const Context& | ||
+ | virtual ~TaxStrategy() {} | ||
+ | }; | ||
+ | /* Tax Algorithm */ | ||
+ | class CNTax : public TaxStrategy { | ||
+ | public: | ||
+ | virtual double Calculate(const Context& | ||
+ | }; | ||
+ | class USTax : public TaxStrategy { | ||
+ | public: | ||
+ | virtual double Calculate(const Context& | ||
+ | }; | ||
+ | class DETax : public TaxStrategy { | ||
+ | public: | ||
+ | virtual double Calculate(const Context& | ||
+ | }; | ||
+ | /* if we wanna add a new tax calculate algorithm... */ | ||
+ | class FRTax : public TaxStrategy { | ||
+ | public: | ||
+ | virtual double Calculate(const Context& | ||
+ | }; | ||
+ | /* stable */ | ||
+ | class SalesOrder { | ||
+ | private: | ||
+ | TaxStrategy* strategy; | ||
+ | public: | ||
+ | //we have the concrete Tax Type (Nation) here | ||
+ | SalesOrder(StrategyFactory* strategyFactory) { | ||
+ | this-> | ||
+ | } | ||
+ | ~SalesOrder() { delete this-> | ||
+ | public double CalculateTax() { | ||
+ | Context context(); | ||
+ | // | ||
+ | double val = strategy-> | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | \\ | ||
+ | 明白了以上的内容,再来看看 // | ||
+ | \\ | ||
+ | > | ||
+ | > | ||
+ | 也就是说,具体的算法是变化的,但抽象的算法是稳定的。如果要维持调用这些算法的客户程序的稳定,最好的办法就是用客户程序调用抽象的算法;然后使用抽象算法的具体实现来进行功能的拓展。 | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * // | ||
+ | * // | ||
+ | * 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个 // | ||
+ | |||
+ | ===Observer=== | ||
+ | |||
+ | 在软件构建的过程中,有时候我们需要为某一些对象建立一种“**通知依赖关系**”——即一个对象(目标对象)的状态如果发生改变,那么所有依赖该对象的对象(观察者)都将得到通知。因为这里存在依赖的关系,按照先前提到的面向对象设计原则,我们应该知道,如果这样的依赖关系过于紧密,会使软件抵御变化的能力大大降低。而观察者模式(// | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们来考虑一个例子:按如下代码所示,我们目前拥有两个类:'' | ||
+ | <code cpp linenums: | ||
+ | class FileSplitter | ||
+ | { | ||
+ | string m_filePath; | ||
+ | int m_fileNumber; | ||
+ | public: | ||
+ | FileSplitter(const string& filePath, int fileNumber) : | ||
+ | m_filePath(filePath), | ||
+ | m_fileNumber(fileNumber), | ||
+ | void split(){ | ||
+ | //...read file | ||
+ | //...split the file and write | ||
+ | for (int i = 0; i < m_fileNumber; | ||
+ | //... | ||
+ | } | ||
+ | } | ||
+ | }; | ||
+ | class MainForm : public Form | ||
+ | { | ||
+ | TextBox* txtFilePath; | ||
+ | TextBox* txtFileNumber; | ||
+ | public: | ||
+ | void Button1_Click() { | ||
+ | string filePath = txtFilePath-> | ||
+ | int number = atoi(txtFileNumber-> | ||
+ | FileSplitter splitter(filePath, | ||
+ | splitter.split(); | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 现在我们需要添加一个显示进度条的功能。按照常理来说,我们应该直接去调用控件类;因为 '' | ||
+ | <code cpp linenums: | ||
+ | | ||
+ | </ | ||
+ | 因为具体的文件分割操作是由 '' | ||
+ | <code cpp linenums: | ||
+ | for (int i = 0; i < m_fileNumber; | ||
+ | //... | ||
+ | float progressValue = m_fileNumber; | ||
+ | //get current progress, handle it to m_progressBar | ||
+ | progressValue = (i + 1) / progressValue; | ||
+ | m_progressBar-> | ||
+ | } | ||
+ | </ | ||
+ | 然后在 | ||
+ | <code cpp linenums: | ||
+ | FileSplitter splitter(filePath, | ||
+ | </ | ||
+ | 功能实现了;但仔细思考一下,以上的设计依赖关系是什么样的? | ||
+ | \\ | ||
+ | \\ | ||
+ | 没错,在这个例子中,无论是主流程 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 当然我们可以像以前一样去找 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 如果考虑一下 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 可以想到的是,不管进度条长啥样,它代表的始终是一个进度。因此,我们可以将进度条抽象为一个一定范围的数值;将这个数值交给不同的控件,就可以实现不同的进度条样式了: | ||
+ | <code cpp linenums: | ||
+ | class IProgress { | ||
+ | public: | ||
+ | virtual void DoProgress(float value) = 0; | ||
+ | virtual ~IProgress() {} | ||
+ | }; | ||
+ | class FileSplitter { | ||
+ | //.... | ||
+ | IProgress* | ||
+ | //.... | ||
+ | }; | ||
+ | </ | ||
+ | 可以看到的是,'' | ||
+ | <code cpp linenums: | ||
+ | class MainForm : public Form, public IProgress { | ||
+ | //.... | ||
+ | virtual void DoProgress(float value) { | ||
+ | progressBar-> | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | \\ | ||
+ | 通过以上的改动,我们已经成功的实现了依赖导致原则:功能类 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 上面的修改只支持了一个流程(观察者)。假如我们有多个观察者需要接收通知(比如要把进度结果通知给不同的进度条),应该怎么做呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | 很简单;前面我们通过抽象对象 '' | ||
+ | <code cpp linenums: | ||
+ | List< | ||
+ | </ | ||
+ | 当然,为了实现对这些通知的管理,我们需要再设计一些函数: | ||
+ | <code cpp linenums: | ||
+ | void addIProgress(IProgress* iprogress) { | ||
+ | m_iprogressList.push_back(iprogress); | ||
+ | } | ||
+ | void removeIProgress(IProgress* iprogress) { | ||
+ | m_iprogressList.remove(iprogress); | ||
+ | } | ||
+ | </ | ||
+ | 通过上述的改写,我们实际上实现了如下图所示的结果: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 具体的实现代码如下: | ||
+ | <code cpp linenums: | ||
+ | class IProgress { | ||
+ | public: | ||
+ | virtual void DoProgress(float value)=0; | ||
+ | virtual ~IProgress() {} | ||
+ | }; | ||
+ | class FileSplitter { | ||
+ | string m_filePath; | ||
+ | int m_fileNumber; | ||
+ | List< | ||
+ | public: | ||
+ | FileSplitter(const string& filePath, int fileNumber) : | ||
+ | m_filePath(filePath), | ||
+ | m_fileNumber(fileNumber) { | ||
+ | } | ||
+ | void split() { | ||
+ | for (int i = 0; i < m_fileNumber; | ||
+ | //... | ||
+ | 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< | ||
+ | while (itor != m_iprogressList.end() ) | ||
+ | (*itor)-> | ||
+ | itor++; | ||
+ | } | ||
+ | } | ||
+ | }; | ||
+ | class MainForm : public Form, public IProgress { | ||
+ | TextBox* txtFilePath; | ||
+ | TextBox* txtFileNumber; | ||
+ | ProgressBar* progressBar; | ||
+ | public: | ||
+ | void Button1_Click( ){ | ||
+ | string filePath = txtFilePath-> | ||
+ | int number = atoi(txtFileNumber-> | ||
+ | ConsoleNotifier cn; | ||
+ | FileSplitter splitter(filePath, | ||
+ | splitter.addIProgress(this); | ||
+ | splitter.addIProgress(& | ||
+ | splitter.split(); | ||
+ | splitter.removeIProgress(this); | ||
+ | } | ||
+ | virtual void DoProgress(float value) { | ||
+ | progressBar-> | ||
+ | } | ||
+ | }; | ||
+ | class ConsoleNotifier : public IProgress { | ||
+ | public: | ||
+ | virtual void DoProgress(float value) { | ||
+ | cout << " | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 再来看看观察者模式的定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * 使用面向对象的抽象,// | ||
+ | * 目标发送通知时,无需制定观察者,通知(可以携带通知信息作为参数)会自动传播。 | ||
+ | * 观察者自己决定是否需要订阅通知,目标对象对此一无所知。 | ||
+ | * // | ||
+ | |||
+ | ====设计模式:单一职责==== | ||
+ | |||
+ | 在软件组件的设计中,继承的概念是很重要的。一个类是否需要继承,是需要根据其是否真正符合继承的含义来决定的。盲目的继承意味着责任划分的不清晰;很多时候,错误的继承会随着需求的变化导致子类急剧的膨胀;而膨胀的元凶则是代码的冗余。因此,我们在软件设计中,必须明确模块的责任。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 有两种设计模式特别强调了责任的界限:装饰模式(// | ||
+ | |||
+ | ===Decorator=== | ||
+ | |||
+ | 某些情况下我们可能会错误的使用继承。由于继承具有为类型引入静态的潜质(必须在编译阶段指定类型),导致很多时候我们的扩展方式都缺乏灵活性(不能去动态的决定)。更糟的是,随着这些子类的增多,以及需要更多功能的扩展,接下来的子类会膨胀到一个惊人的程度。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一个具体的例子: | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们知道流(// | ||
+ | <code cpp linenums: | ||
+ | /* Base Class with operations */ | ||
+ | class Stream { | ||
+ | public: | ||
+ | virtual char Read(int number) = 0; | ||
+ | virtual void Seek(int position) = 0; | ||
+ | virtual void Write(char data) = 0; | ||
+ | virtual ~Stream() {} | ||
+ | }; | ||
+ | |||
+ | class FileStream: | ||
+ | public: | ||
+ | virtual char Read(int number) { .... } // read FileStream | ||
+ | virtual void Seek(int position) { .... } // seek FileStream | ||
+ | virtual void Write(char data) { .... } // write FileStream | ||
+ | }; | ||
+ | |||
+ | class NetworkStream :public Stream { | ||
+ | public: | ||
+ | virtual char Read(int number) { .... } // read NetworkStream | ||
+ | virtual void Seek(int position) { .... } // seek NetworkStream | ||
+ | virtual void Write(char data) { .... } // write NetworkStream | ||
+ | }; | ||
+ | |||
+ | class MemoryStream :public Stream { | ||
+ | public: | ||
+ | virtual char Read(int number) { .... } // read MemoryStream | ||
+ | virtual void Seek(int position) { .... } // seek MemoryStream | ||
+ | virtual void Write(char data) { .... } // wr MemoryStream | ||
+ | }; | ||
+ | </ | ||
+ | 那么此时的继承关系应该是这样的: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 如果有一天,我们想对这些个类做添加一个加密操作;如果通过继承来扩展功能,那么结构图就会变成这样: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 如果再有一天,我们又想添加一个缓冲操作,如果又通过继承来扩展功能,那么结构图就会更复杂: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 如果再有一天我们希望对我们主体类同时使用加密和缓冲拓展…… | ||
+ | \\ | ||
+ | 反正我是不想画了……而如你肉眼所见,仅仅添加了两个功能,我们的新子类就膨胀到了令人发指的地步。\\ | ||
+ | \\ | ||
+ | 造成这样的原因是因为我们错误的使用了继承;换句话说,我们模糊了各个类的责任。对于三个主体类(文件流、网络流、内存流),继承关系是正确的;因为他们确实反映了与 //Stream// 的 //Is-a// 关系。但反观我们后来加上去的加密和缓冲,这些拓展性的功能是否真的需要用到继承呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一下具体的代码(以加密为例子): | ||
+ | <code cpp linenums: | ||
+ | class CryptoFileStream :public FileStream{ | ||
+ | public: | ||
+ | virtual char Read(int number){ | ||
+ | //addtional crypto OP... | ||
+ | FileStream:: | ||
+ | } | ||
+ | virtual void Seek(int position){ | ||
+ | //addtional crypto OP... | ||
+ | FileStream:: | ||
+ | //addtional crypto OP... | ||
+ | } | ||
+ | virtual void Write(byte data){ | ||
+ | //addtional crypto OP... | ||
+ | FileStream:: | ||
+ | // | ||
+ | } | ||
+ | }; | ||
+ | class CryptoNetworkStream : :public NetworkStream{ | ||
+ | public: | ||
+ | virtual char Read(int number){ | ||
+ | //addtional crypto OP... | ||
+ | NetworkStream:: | ||
+ | } | ||
+ | virtual void Seek(int position){ | ||
+ | //addtional crypto OP... | ||
+ | NetworkStream:: | ||
+ | //addtional crypto OP... | ||
+ | } | ||
+ | virtual void Write(byte data){ | ||
+ | //addtional crypto OP... | ||
+ | NetworkStream:: | ||
+ | //addtional crypto OP... | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | </ | ||
+ | 通过查看上面的代码,我们发现**基本操作下的**绝大部分的代码都是在做重复做同样的工作;也就是说,如果按照这样的结构来写程序,代码的冗余会非常高。找到了重复点,那么我们有没有办法来消除这些重复? | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们接着仔细观察,这些操作的不同在哪里? | ||
+ | <code cpp linenums: | ||
+ | FileStream:: | ||
+ | NetworkStream:: | ||
+ | </ | ||
+ | 可以看到的是,除了调用的类不一样,方法 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 那么我们可以尝试着将继承的关系改为组合的关系试试,于是就有了: | ||
+ | <code cpp linenums: | ||
+ | class CryptoNetworkStream { | ||
+ | NetworkStream *stream; //switch inheritance to combination | ||
+ | ..... | ||
+ | }; | ||
+ | </ | ||
+ | 既然类中有了对应的主体类的对象,很显然我们可以通过该对象去调用对应的基本操作,于是就有了: | ||
+ | <code cpp linenums: | ||
+ | class CryptoNetworkStream { | ||
+ | NetworkStream *stream; //switch inheritance to combination | ||
+ | virtual void Seek(int position) { | ||
+ | stream -> seek(position); | ||
+ | } | ||
+ | ..... | ||
+ | }; | ||
+ | </ | ||
+ | 对比之前通过继承来实现的调用 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们接着将所有的加密子类都替换成上面的组合方式调用。当你替换完成的时候,你又会发现一件有趣的事情: | ||
+ | <code cpp linenums: | ||
+ | class CryptoFileStream { | ||
+ | FileStream *stream; //switch inheritance to combination | ||
+ | virtual void Seek(int position) { | ||
+ | stream -> seek(position); | ||
+ | }; | ||
+ | class CryptoNetworkStream { | ||
+ | NetworkStream *stream; //switch inheritance to combination | ||
+ | virtual void Seek(int position) { | ||
+ | stream -> seek(position); | ||
+ | }; | ||
+ | </ | ||
+ | 看到这里,我们突然明白了:上下的两个 '' | ||
+ | <code cpp linenums: | ||
+ | class CryptoFileStream { | ||
+ | Stream *stream; //new Filestream | ||
+ | virtual void Seek(int position) { | ||
+ | stream -> seek(position); | ||
+ | }; | ||
+ | class CryptoNetworkStream { | ||
+ | Stream *stream; //new NetworkStream | ||
+ | virtual void Seek(int position) { | ||
+ | stream -> seek(position); | ||
+ | }; | ||
+ | </ | ||
+ | 以上的修改将所有的变化都丢到了运行时;也就是说,这几个看似不同的类,在编译阶段是完全等同的。他们唯一的不同,是在 Run time 决定的。因此,我们实际上就可以把上述的这种为了添加扩展功能而设计的众多子类合并到一起了。而因为我们这里有虚函数的存在,因此必须继承基类 //Stream// 来**指定接口规范**: | ||
+ | <code cpp linenums: | ||
+ | class CryptoStream : public Stream { | ||
+ | Stream *stream; //... | ||
+ | public: | ||
+ | virtual void Seek(int position) { | ||
+ | stream -> seek(position); | ||
+ | }; | ||
+ | </ | ||
+ | 这就是装配模式的一大特点:既继承基类,又组合基类(这两个是完全不同的概念)。到此,我们成功的将加密操作做成了一个独立的模块;而这个模块在对之后主体类应用的过程中,呈现出了一种相加的状态: | ||
+ | <code cpp linenums: | ||
+ | FileStream* s1 = new Filestream(); | ||
+ | CryptoStream* s2 = new CryptoStream(s1); | ||
+ | </ | ||
+ | <WRAP center round info 100%> | ||
+ | 注:拓展功能类同样需要构造函数。 | ||
+ | </ | ||
+ | |||
+ | \\ | ||
+ | \\ | ||
+ | 而拓展操作独立的好处在于,我们可以对任意的主体类进行叠加式的操作,而这样叠加式的操作避免了像继承那样的大量重复性操作。这样的操作,称之为**运行时装配**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 根据 //GoF// 的经典重构理论来说,如果某一个类它的多个子类都具有同样字段的时候,那么应该将这个字段晚上提。比如我们的两个拓展功能类加密和缓冲,内部都有这么一段: | ||
+ | <code cpp linenums: | ||
+ | Stream* stream; | ||
+ | </ | ||
+ | 我们可以将这一段提到基类 // | ||
+ | <code cpp linenums: | ||
+ | DecoratorStream: | ||
+ | protected: | ||
+ | Stream* stream;// | ||
+ | DecoratorStream(Stream * stm): | ||
+ | }; | ||
+ | </ | ||
+ | 做好这个中间类以后,以后所有的拓展功能类,以及需要通过组合基类来实现操作的子类,都可以继承这个类了。而经过这么一段折腾,我们的类关系实际上变成了这样: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 到此,我们将所有的类功能都独立了出来,而我们的主题类也不用再依赖拓展功能类,就可以直接进行编译了。是不是太奇妙了!!这就是// | ||
+ | \\ | ||
+ | \\ | ||
+ | 同时可以看出来的是,通过这样的设计模式,我们对整个系统的改进是巨大的;乘法和加法的数量级永远不是在一个等级上的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 说到这里,我们也能看出来装配模式到底可以解决什么样的问题了: | ||
+ | \\ | ||
+ | \\ | ||
+ | 在某些情况下,我们可能会“过度的使用继承来扩展对象的功能”,由于继承为类型引入静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的在增多),各种子类的组合(扩展功能的组合)会导致子类的膨胀。这样的情况下就需要用到装配模式。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一看定义: | ||
+ | > | ||
+ | > | ||
+ | 这个结果跟我们上面优化前后的对比结果是不是一致呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * 通过采用组合而非继承的手法,// | ||
+ | * // | ||
+ | * // | ||
+ | |||
+ | ===Bridge=== | ||
+ | |||
+ | 单一职责类型的设计模式主要解决的是责任划分的问题。我们在 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 细想一下这个问题实际和先前讨论的问题非常相似:上一节谈到的子类快速膨胀的后果,就是因为没有明确模块责任而导致的。而在这里,类型的实现逻辑需要往不同方向变化则表明了该类型需要实现多个功能,而这些功能可能按自身的意义分为好几个范畴不同的部分。如果把这些功能都混淆在一起,在以后的调用中很可能会出现问题(调用很可能只需要一部分功能的变化)。因此,我们在设计中,不但需要强调功能与主体的分离,而且需要将不同类型的功能区别看待。**桥模式** //Bridge// 正好可以解决这样的问题。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 还是从一个例子开始吧。假设我们需要开发一套即时通讯软件,该软件需要在不同的平台上部署(PC,移动端),而每个平台都要求有类似功能的具体实现。考虑到每个平台的功能需求相同,但实现不同,我们首先想到的就是针对不同的平台继承不同的基类,然后再进行拓展功能。这样的类关系可以表现为下图: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 有一天我们希望在每个平台上都开发出两个不同的版本。两个版本具有不同的功能,但用到的基础功能是一致的。因此,我们的设计可能就会演变为下图所示: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 这个结构是不是很熟悉?我们希望开发的两个版本只是基于公用的基础功能实现了不同的功能组合,但这样的设计给出的结果却是将新增的功能组合作为了主体类的一部分。再来看看实现: | ||
+ | <code cpp linenums: | ||
+ | /* Base class */ | ||
+ | class Messager { | ||
+ | public: | ||
+ | virtual void Login(string username, string password)=0; | ||
+ | virtual void SendMessage(string message)=0; | ||
+ | virtual void SendPicture(Image image)=0; | ||
+ | |||
+ | virtual void PlaySound()=0; | ||
+ | virtual void DrawShape()=0; | ||
+ | virtual void WriteText()=0; | ||
+ | virtual void Connect()=0; | ||
+ | | ||
+ | virtual ~Messager(){} | ||
+ | }; | ||
+ | /* platform Implementation */ | ||
+ | class PCMessagerBase : public Messager{ | ||
+ | public: | ||
+ | virtual void PlaySound() { .... } | ||
+ | virtual void DrawShape() { .... } | ||
+ | virtual void WriteText() { .... } | ||
+ | virtual void Connect(){ .... } | ||
+ | }; | ||
+ | class MobileMessagerBase : public Messager{ | ||
+ | public: | ||
+ | virtual void PlaySound() { .... } | ||
+ | virtual void DrawShape() { .... } | ||
+ | virtual void WriteText() { .... } | ||
+ | virtual void Connect(){ .... } | ||
+ | }; | ||
+ | |||
+ | class PCMessagerLite : public PCMessagerBase { | ||
+ | public: | ||
+ | virtual void Login(string username, string password) { PCMessagerBase:: | ||
+ | virtual void SendMessage(string message) { PCMessagerBase:: | ||
+ | virtual void SendPicture(Image image) { PCMessagerBase:: | ||
+ | }; | ||
+ | class PCMessagerPerfect : public PCMessagerBase { | ||
+ | public: | ||
+ | virtual void Login(string username, string password) { PCMessagerBase:: | ||
+ | virtual void SendMessage(string message) { PCMessagerBase:: | ||
+ | virtual void SendPicture(Image image) { PCMessagerBase:: | ||
+ | }; | ||
+ | class MobileMessagerLite : public MobileMessagerBase { | ||
+ | public: | ||
+ | virtual void Login(string username, string password) { MobileMessagerBase :: | ||
+ | virtual void SendMessage(string message) { MobileMessagerBase :: | ||
+ | virtual void SendPicture(Image image) { MobileMessagerBase :: | ||
+ | }; | ||
+ | class MobileMessagerPerfect : public MobileMessagerBase { | ||
+ | public: | ||
+ | virtual void Login(string username, string password) { | ||
+ | MobileMessagerBase:: | ||
+ | MobileMessagerBase:: | ||
+ | } | ||
+ | virtual void SendMessage(string message) { | ||
+ | MobileMessagerBase:: | ||
+ | MobileMessagerBase:: | ||
+ | } | ||
+ | virtual void SendPicture(Image image) { | ||
+ | MobileMessagerBase:: | ||
+ | MobileMessagerBase:: | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 可见的是,上面的代码有大量的重复。对于不同平台的不同版本在实现中,唯一不同的就是他们继承的类不同。按照我们在装配模式中学到的经验,这是可以通过继承转组合化简的,也就是把我们需要的不同版本(不同基础功能的组合)作为一个扩展功能来看待。因此,上面的一大堆子类也可以按照装配模式的方法来精简,于是可以化简成: | ||
+ | <code cpp linenums: | ||
+ | class MessagerLite { | ||
+ | MessagerImp* messagerImp; | ||
+ | public: | ||
+ | virtual void Login(string username, string password) { messagerImp-> | ||
+ | virtual void SendMessage(string message){ messagerImp-> | ||
+ | virtual void SendPicture(Image image) { messagerImp-> | ||
+ | }; | ||
+ | class MessagerPerfect { | ||
+ | MessagerImp* messagerImp; | ||
+ | public: | ||
+ | virtual void Login(string username, string password) { | ||
+ | messagerImp-> | ||
+ | messagerImp-> | ||
+ | .... | ||
+ | } | ||
+ | virtual void SendMessage(string message) { | ||
+ | messagerImp-> | ||
+ | messagerImp-> | ||
+ | .... | ||
+ | } | ||
+ | virtual void SendPicture(Image image){ | ||
+ | messagerImp-> | ||
+ | messagerImp-> | ||
+ | .... | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 到这里,添加不同功能组合的计划已经被实现了。不过回到先前的源代码中,我们发现有这么一段: | ||
+ | <code cpp linenums: | ||
+ | class PCMessagerBase : public Messager { ... } | ||
+ | </ | ||
+ | 仔细看一下,这里是有问题的。 '' | ||
+ | \\ | ||
+ | 那要怎样组织类关系才合理呢?我们不妨来理顺一下思路: | ||
+ | - '' | ||
+ | - 而上述这三个函数的实现,又包含了对后面四个函数的重写:比如 '' | ||
+ | 因此,我们明白一件事:'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 有了这个思路,我们可以做几件事来改进我们的程序了: | ||
+ | - '' | ||
+ | - 接下来,'' | ||
+ | - 再者,我们分离出来的函数组成的类 '' | ||
+ | \\ | ||
+ | 做完这几步,我们发现我们将实现和抽象又一次的分开了,先前通过直接继承的复杂关系,到此也理清楚了。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 总的来说,这就是一个抽象类会有自身的诠释,而诠释的过程需要交给具体的其他函数去实现。回头看看本节概述中关于桥模式所针对的问题的描述,也就明白了。在这里,变化的有两种:一种是抽象类的子类的变化;另外一种是子类中具体实现的函数的变化。把这两者分开后,两边的责任都非常明确。接着再以组合的方式拼到一起,我们就能得到一个清晰的程序结构,和更加简洁高效的代码。这就是桥模式所带给我们的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看看桥模式的定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Bridge// 模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,即“子类化”他们。 | ||
+ | * //Bridge// 模式有时候类似于多继承方案,但是多继承方案往往违背单一指责原则(即一个类只有一个变化的原因),复用性较差。// | ||
+ | * //Bridge// 模式的应用一般在“两个非常强的变化维度”,有时一个类也有多于两个的变化维度,这是可以使用 //Bridge// 的扩展模式。 | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | ===== ===== | ||
+ | ~~DISQUS~~ |