本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版 | |||
cs:programming:cpp:boolan_cpp:design_pattern_2 [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:boolan_cpp:design_pattern_2 [2024/01/14 13:46] (当前版本) – ↷ 页面programming:cpp:boolan_cpp:design_pattern_2被移动至cs:programming:cpp:boolan_cpp:design_pattern_2 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======C++设计模式 第二周====== | ||
+ | 本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\ | ||
+ | <wrap em> | ||
+ | ---- | ||
+ | ====设计模式:对象创建==== | ||
+ | |||
+ | 传统的对象创建一般是通过栈上创建或者是 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 对象创建类型设计模式的典型例子有: | ||
+ | *// Factory Method// | ||
+ | *// Abstract Factory// | ||
+ | *// Prototype// | ||
+ | * //Builder// | ||
+ | |||
+ | ===Factory Method=== | ||
+ | |||
+ | 在软件设计中,经常要面临创建对象的工作。由于需求的变化,需要创建的对象往往也是需要变化的。我们知道,传统常规意义上的对象创建方法会导致紧耦合,那么有没有一种方法来避免这样的紧耦合呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一个具体的例子。我们在前面分析过一个文件分割的例子;现在我们假设这个文件分割的算法有好多种。那么如果从变化的场景来考虑的话,这个问题应该怎么解决? | ||
+ | \\ | ||
+ | \\ | ||
+ | 从面向对象设计的角度来考虑,我们应该设计一个抽象基类,在子类创建对象的过程中使用基类对象来创建,这样可以保证创建对象时声明的类的类型和对象都是抽象的。这样的声明可以让我们将对象的类型判断放置到运行时去决定。当然,针对这样的写法,我们需要通过子类继承基类,然后在子类中重写对象创建的定义来实现,比如如下代码结构: | ||
+ | <code cpp linenums: | ||
+ | class ISplitter { | ||
+ | public: | ||
+ | virtual void split()=0; | ||
+ | virtual ~ISplitter(){} | ||
+ | }; | ||
+ | class BinarySplitter : public ISplitter { .... }; | ||
+ | class TxtSplitter: | ||
+ | class PictureSplitter: | ||
+ | class VideoSplitter: | ||
+ | </ | ||
+ | 这样的写法在构思上是没有问题的。我们设计类的时候,一般都会用抽象类来代替具体类。这样的写法是严格遵循依赖倒置原则的,我们需要将对象的创建依赖到一个稳定的类型上。这样的编程手法,我们称之为**面向接口编程**。不过如果按照这样的写法,我们会发现一个问题:**抽象类是不能直接用于对象创建的**。如我要创建一个 '' | ||
+ | <code cpp linenums: | ||
+ | BinarySplitter splitter; //create object on stack | ||
+ | ISplitter * splitter = new BinarySplitter(filePath, | ||
+ | </ | ||
+ | 这样的写法必然要求编译时的细节依赖,显然是违反依赖倒置原则的。那我们应该怎么做呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | //Factory Method// 就是用于解决这个问题的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 在上面的代码中,我们可以看到:如果使用 '' | ||
+ | <code cpp linenums: | ||
+ | ISplitter * splitter = new BinarySplitter(filePath, | ||
+ | </ | ||
+ | 赋值操作符左边已经通过抽象类实现了面向接口编程,因此我们只需要思考如何将右边的内容也实现面向接口编程就可以了。我们想到,对象除了被创建,还是可以作为函数的返回值返回的;因此我们可以尝试将对象的创建设计为一个函数(方法),然后用该函数返回创建好的对象: | ||
+ | <code cpp linenums: | ||
+ | class SplitterFactory { | ||
+ | public: | ||
+ | ISplitter* CreateSplitter() { | ||
+ | return new BinarySplitter(filePath, | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 但是这样写还是没有解决根本的问题: | ||
+ | <code cpp linenums: | ||
+ | SplitterFactory factory; | ||
+ | ISplitter * splitter = factory.CreateSplitter(); | ||
+ | </ | ||
+ | 我们发现 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 其实换一下思路,我们现在无非就是需要避免编译时的具体类型依赖。那么顺着这样的思路,我们很快就能想到,如果可以把 CreateSplitter() 中的对象类型决定丢到运行时去决定的话,是不是就可以了? | ||
+ | \\ | ||
+ | \\ | ||
+ | C++中怎样才能使类型依赖延迟?没错,就是**虚函数和多态**。既然 '' | ||
+ | <code cpp linenums: | ||
+ | class SplitterFactory { | ||
+ | public: | ||
+ | virtual ISplitter* CreateSplitter() = 0; | ||
+ | virtual ~SplitterFactory(); | ||
+ | }; | ||
+ | </ | ||
+ | 既然 '' | ||
+ | <code cpp linenums: | ||
+ | SplitterFactory *factory; | ||
+ | ISplitter * splitter = factory -> CreateSplitter(); | ||
+ | </ | ||
+ | 啊哈,这个不就是多态的标准模样吗? | ||
+ | \\ | ||
+ | \\ | ||
+ | 那剩余的问题就是处理我们的基类对象 '' | ||
+ | <code cpp linenums: | ||
+ | class BinarySplitterFactory: | ||
+ | public: | ||
+ | virtual ISplitter* CreateSplitter(){ return new BinarySplitter(); | ||
+ | }; | ||
+ | class TxtSplitterFactory: | ||
+ | public: | ||
+ | virtual ISplitter* CreateSplitter() { return new TxtSplitter(); | ||
+ | }; | ||
+ | class PictureSplitterFactory: | ||
+ | public: | ||
+ | virtual ISplitter* CreateSplitter(){ return new PictureSplitter(); | ||
+ | }; | ||
+ | class VideoSplitterFactory: | ||
+ | public: | ||
+ | virtual ISplitter* CreateSplitter() { return new VideoSplitter(); | ||
+ | }; | ||
+ | </ | ||
+ | 到此,我们发现我们每一个类都有一个具体的工厂实现。现在的流程,就从我们直接去创建一个具体类型的对象,改为了交给我们的工厂基类去创建;而工厂基类通过子类的重写来创建指定类型的对象了。形式上,我们将这样的使用方法称为**多态** '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 当然,你可能会说,在具体的 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 就该软件整体的设计来说,这是没有关系的。通过上面的设计,我们在创建对象的过程中,再也没有需要具体依赖的类型了。我们的创建过程,依赖的是一个稳定的抽象类。而我们设计的理念,并不是要把变化都消灭掉,而是把这些变化都约束到一个指定的区域,使得整个程序更加有序,可控。这也是 //Factory Method// 处理该问题的思想。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一看 //Factory Method// 设计模式的定义吧: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Factory Method// 模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(比如 '' | ||
+ | * //Factory Method// 模式通过面向对象的手法,将所要创建的具体对象的工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好的解决了这种紧耦合关系。 | ||
+ | * //Factory Method// 模式解决了“单个对象”的需求变化,但其缺点在于要求创建的**方法和参数相同**。 | ||
+ | |||
+ | ===Abstract Factory=== | ||
+ | |||
+ | //Abstract Factory//** 抽象工厂**模式是从 //Factory Method// 工厂模式中衍生出来的;它和 //Factory Method// 唯一不同的区别在于, //Factory Method// 是对单一对象进行逐个创建处理,而 //Abstract Factory// 需要处理**一系列互相依赖的对象**的创建工作。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 什么叫“一系列互相依赖的对象”?我们把这个条件分解一下,可以得到两个结论: | ||
+ | * //Abstract Factory// 处理的对象创建有很多个 | ||
+ | * //Abstract Factory// 处理的对象是具有相互依赖的特性的 | ||
+ | \\ | ||
+ | 接着来看看具体的例子: | ||
+ | \\ | ||
+ | \\ | ||
+ | 假设我们需要设计一个数据库连接系统。数据库的一般连接实现都分为了好几个步骤。如果只考虑具体的实现方法,那么一个数据库连接系统的基本思路是这样的: | ||
+ | <code cpp linenums: | ||
+ | //create connection | ||
+ | SqlConnection* connection = new SqlConnection(); | ||
+ | connection-> | ||
+ | //using commend | ||
+ | SqlCommand* command = new SqlCommand(); | ||
+ | command-> | ||
+ | command-> | ||
+ | //read data | ||
+ | SqlDataReader* reader = command-> | ||
+ | .... | ||
+ | </ | ||
+ | 这是一个具体的实现(SQL数据库的实现);考虑到根据数据库的不同;那么 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 这让人不经想到了工厂模式:要把创建对象的过程抽象化,稳定化,工厂模式是搞的定的。因此,按照工厂模式的思路来,首先应该就是按照面向接口编程的方法,把所有的具体实现抽象出类(接口)来,并配套上对应的工厂抽象类: | ||
+ | <code cpp linenums: | ||
+ | class IDBConnection{ .... }; | ||
+ | class IDBConnectionFactory{ | ||
+ | public: | ||
+ | virtual IDBConnection* CreateDBConnection()=0; | ||
+ | }; | ||
+ | class IDBCommand{ | ||
+ | }; | ||
+ | class IDBCommandFactory { | ||
+ | public: | ||
+ | virtual IDBCommand* CreateDBCommand()=0; | ||
+ | }; | ||
+ | class IDataReader{ .... }; | ||
+ | class IDataReaderFactory { | ||
+ | public: | ||
+ | virtual IDataReader* CreateDataReader()=0; | ||
+ | }; | ||
+ | </ | ||
+ | 按照工厂模式的实现方法,我们需要对上面每一个工厂抽象类进行具体的实现。比如我们要实现SQL数据库的具体链接方法,就需要写出如下的代码: | ||
+ | <code cpp linenums: | ||
+ | //sql connection implementation | ||
+ | class SqlConnection: | ||
+ | class SqlConnectionFactory: | ||
+ | //sql commend implementation | ||
+ | class SqlCommand: public IDBCommand { .... }; | ||
+ | class SqlCommandFactory: | ||
+ | //sql data read implementation | ||
+ | class SqlDataReader: | ||
+ | class SqlDataReaderFactory: | ||
+ | </ | ||
+ | 到这里问题似乎解决了;具体的实现已经交给工厂的多态去处理了。但实际上这段代码有很大的问题。仔细想一下数据库的链接方式,我们发现以上这三个方法实际上需要配套使用的。举个例子:如果我们使用 sql 的 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 反观我们在抽象类中写出的代码,我们发现因为我们将所有的类型决定交给了运行时处理,因此在运行时的类型决定,其实是我们自己说了算的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 也就是说,针对 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 那应该怎么去修改呢?其实很简单,把这三个工厂合并为一就可以了。既然这些对象互相依赖,那么很显然,他们可以在一个序列里实现: | ||
+ | <code cpp linenums: | ||
+ | class SqlDBFactory: | ||
+ | public: | ||
+ | virtual IDBConnection* CreateDBConnection() = 0; | ||
+ | virtual IDBCommand* CreateDBCommand() = 0; | ||
+ | virtual IDataReader* CreateDataReader() = 0; | ||
+ | }; | ||
+ | //concrete stuffs | ||
+ | class SqlConnection: | ||
+ | class SqlCommand: public IDBCommand { .... }; | ||
+ | class SqlDataReader: | ||
+ | </ | ||
+ | \\ | ||
+ | 看到这里,你也应该明白这实际上就是一个特化的,用于处理多个相互依赖对象的工厂模式了。这就是 //Abstract Factory// 的核心概念:将**一系列互相依赖的对象**整合到一起使用工厂处理。它在本质上也是属于工厂模式的一种,但因为牵涉到多个相互依赖对象,实际上的处理又显得稍有不同。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看看 //Abstract Factory// 的具体定义吧: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * 如果没有应对“多系列对象构建”的需求变化,则没有必要使用 //Abstract Factory// 模式,使用简单的工厂完全足以胜任。 | ||
+ | * “一系列对象”指的是在**某一特定系列下的对象**之间具有相互依赖或作用的关系。不同系列的对象之间是不存在相互依赖的关系的。 | ||
+ | * //Abstract Factory// 模式主要在于应对以系列作为变化的单位的需求变动。相对于处理单个对象的工厂模式来说,它的缺点在于难以应对以对象为单位的需求变动上。 | ||
+ | |||
+ | ===Prototype=== | ||
+ | |||
+ | 有些时候我们需要创建一些很复杂的对象:比如构造函数的参数一大堆,而我们又看不懂参数到底在说啥(STL里很多这样的东西);或者比如我们创建了一个对象,然后慢慢的添砖加瓦做到了相当的复杂程度。某个时间我们想获取一个该对象的副本,发现原有的工厂方法创建的对象比修改过的这个差远了,要想弄还得一步一步再加上去;或者在开始设计软件的时候,你压根就不知道你未来需要添加什么样的功能。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 在这些情况下,使用 // | ||
+ | \\ | ||
+ | \\ | ||
+ | // | ||
+ | <code cpp linenums: | ||
+ | class ISplitter { | ||
+ | public: | ||
+ | virtual void split()=0; | ||
+ | virtual ISplitter* clone() = 0; // | ||
+ | virtual ~ISplitter() {} | ||
+ | }; | ||
+ | </ | ||
+ | 上面这个 '' | ||
+ | <code cpp linenums: | ||
+ | /* Concrete class */ | ||
+ | class BinarySplitter : public ISplitter{ | ||
+ | public: | ||
+ | virtual ISplitter* clone() { return new BinarySplitter(*this); | ||
+ | }; | ||
+ | class TxtSplitter: | ||
+ | public: | ||
+ | virtual ISplitter* clone() { return new TxtSplitter(*this); | ||
+ | }; | ||
+ | class PictureSplitter: | ||
+ | public: | ||
+ | virtual ISplitter* clone() { return new PictureSplitter(*this); | ||
+ | }; | ||
+ | class VideoSplitter: | ||
+ | public: | ||
+ | virtual ISplitter* clone() { return new VideoSplitter(*this); | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的是,在使用的时候,原型对象是不能直接用于调用方法的。我们必须先创建原型对象的拷贝,再用这个拷贝去调用方法: | ||
+ | <code cpp linenums: | ||
+ | ISplitter * splitter = prototype-> | ||
+ | splitter-> | ||
+ | </ | ||
+ | 原型模式的实质就是通过现有的对象,再复制一个新的对象出来。和诸多对象创建设计模式一样,它也绕开了直接使用 '' | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | |||
+ | ===Builder=== | ||
+ | |||
+ | 在软件的构建过程中,我们有时候会创建这样的对象:对象包含着各种部分;每个**部分**都经常**变化**,这些部分的组成结构却十分稳定。那么有没有一种封装机制可以将组成结构部分(稳定部分)隔离出来,从而保证该结构不受部分的变化而改变呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | 这样相似的情况我们在 //Template Method// 模式中见过。在创建对象模式中,我们有一种单独的设计模式 //Builder// **构建器**来解决这个问题。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一个具体的例子。我们现在有一个游戏场景需要建一所房子,房子的类型可能不同,但建房的流程是一定的:比如打地基,墙壁,窗户,屋顶等等,这些是无论建造哪种房子都一定要做的是事情。不过每一个过程的都可能有变化:墙壁可能材质不同,窗户可能样式不同等等。因此,按照这样的思维,我们首先可以设计出一个初始化函数(稳定)用于主要的流程,然后再在这个函数中调用其不同版本的步骤函数(变化): | ||
+ | <code cpp linenums: | ||
+ | class House { | ||
+ | public: | ||
+ | /* House Builder */ | ||
+ | void Init() { | ||
+ | | ||
+ | | ||
+ | | ||
+ | ..... | ||
+ | } | ||
+ | | ||
+ | protected: | ||
+ | House* pHouse; | ||
+ | virtual void BuildPart1() = 0; | ||
+ | virtual void BuildPart2() = 0; | ||
+ | virtual void BuildPart3() = 0; | ||
+ | virtual void BuildPart4() = 0; | ||
+ | virtual void BuildPart5() = 0; | ||
+ | }; | ||
+ | </ | ||
+ | // | ||
+ | \\ | ||
+ | \\ | ||
+ | 当有了这个基类以后,我们就可以直接在子类里重写这些步骤函数了。比如我们要建一个石头房子: | ||
+ | <code cpp linenums: | ||
+ | class StoneHouse: public House{ | ||
+ | virtual void BuildPart1(){ // | ||
+ | virtual void BuildPart2(){ .... } | ||
+ | virtual void BuildPart3(){ .... } | ||
+ | virtual void BuildPart4(){ .... } | ||
+ | virtual void BuildPart5(){ .... } | ||
+ | }; | ||
+ | </ | ||
+ | 到这里实际上我们需要的功能基本都实现了。子类中只需重写属于**部分**的内容就可以;而结构部分(建造房子的基本顺序)是稳定的,不会被改变的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 不过上述的代码还存在一些优化空间。有时候我们的对象除了基础结构的内容,还会有其他的杂七杂八的功能。如果混淆在一起,会使对象的构建过程变得更加复杂。为此,我们需要将上述的功能分离。这样做将使对象更为轻便,程序结构更加清晰。上述例子中,我们就可以把 '' | ||
+ | <code cpp linenums: | ||
+ | class House { .... }; | ||
+ | /* Construction Part */ | ||
+ | class HouseBuilder { | ||
+ | public: | ||
+ | void Init() { | ||
+ | | ||
+ | | ||
+ | | ||
+ | ..... | ||
+ | } | ||
+ | // function that fetch the result | ||
+ | | ||
+ | return pHouse; | ||
+ | } | ||
+ | virtual ~HouseBuilder(){} | ||
+ | protected: | ||
+ | House* pHouse; | ||
+ | virtual void BuildPart1()=0; | ||
+ | virtual void BuildPart2()=0; | ||
+ | virtual void BuildPart3()=0; | ||
+ | virtual void BuildPart4()=0; | ||
+ | virtual void BuildPart5()=0; | ||
+ | }; | ||
+ | </ | ||
+ | 其实到现在程序已经很完善了。当然,我们可以进一步的做拆分,将整个初始化的过程再拆出去,通过一个 '' | ||
+ | <code cpp linenums: | ||
+ | class HouseDirector{ | ||
+ | public: | ||
+ | HouseBuilder* pHouseBuilder; | ||
+ | HouseDirector(HouseBuilder* pHouseBuilder){ | ||
+ | this-> | ||
+ | } | ||
+ | House* Construct(){ | ||
+ | pHouseBuilder-> | ||
+ | pHouseBuilder-> | ||
+ | pHouseBuilder-> | ||
+ | pHouseBuilder-> | ||
+ | pHouseBuilder-> | ||
+ | return pHouseBuilder-> | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 到此,我们已经完成了一个成熟的 //Builder// 版本。之所以做这些额外的步骤,是因为需要将对象的结构和表现做分离,使得同样的构建过程创建不同的表示。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一下 //Builder// 的定义: | ||
+ | > | ||
+ | >—— 《设计模式》// | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Builder// 模式主要用于对象的构建需要按照一定的步骤来实现的状况,而这一定的步骤是一个稳定的结构算法,步骤的内容则会经常变化。 | ||
+ | * //Builder// 模式需要关注变化点的位置。一般来说,哪里有变化,我们就把哪里封装起来。对于部分的频繁变化正是 //Builder// 模式需要处理的问题,但也带来一个副作用:// | ||
+ | * 在 //Builder// 模式中,对于不同语言,其构造器调用虚函数的方式可能有区别。 | ||
+ | |||
+ | ====设计模式:接口隔离==== | ||
+ | |||
+ | 我们在写一些功能比较多的程序时,经常会无意识的模糊不同模块之间的边界。有时候,我们会直接在一个层中调用用另一个层的方法。我们知道,直接调用带来的依赖就是紧耦合;随着软件的复杂,这样的行为增多以后,我们会发现这种直接依赖彼此接口的行为会大量增加对象之间的耦合度。在这样的情况下,如果我们需要修改被依赖的层级中的内容,就需要将依赖该内容的大量内容重新修改。因此,我们有必要使用一个间接层来组织和管理这些复杂的信息。接口隔离设计模式的思想正是应此而生的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 接口隔离的核心思想是**间接**(// | ||
+ | \\ | ||
+ | \\ | ||
+ | 典型的接口隔离设计模式主要有以下四种: | ||
+ | * //Façade// | ||
+ | * //Proxy// | ||
+ | * //Adapter// | ||
+ | * // | ||
+ | |||
+ | ===Façade=== | ||
+ | |||
+ | //Façade// **门面模式**,与其说是一种模式,更不如说是一种设计的方法。它强调实现一个**稳定的接口层**,并依赖该接口层实现对应用端的稳定性。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 一个典型的例子就是数据访问系统。通常我们访问内部数据有好多种方法,而数据的类型又有好多种。如果我们按照直接使用接口的方法来进行访问,那么会产生较高耦合度的依赖: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 一旦内部某个数据或者方法对象接口发生变更,那么所有依赖其实现的外部对象均需要重新更新。这样的关系显然对于客户端来说是及其不友好的。而 //Façade// 模式则提供了一个应用层,并保证这个应用层具有稳定的接口。该接口对外稳定;因此无论内部的方法或结构如何变化,只要对外遵从应用层的接口,那么客户端的设计就不用根据内部的变动而做出相应的变动: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 通过 //Façade// 模式,我们实现了内部和外部的隔离。通过接口层的稳定,我们提高了外部对内部系统的复用度。// | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一看 //Façade// 模式的具体定义: | ||
+ | >// | ||
+ | >—— 设计模式 GoF | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * 从客户程序的角度来看, //Façade// 模式简化了整个组件系统的接口。对于内部和外部客户程序来说, //Façade// 模式实现了一种解耦的效果——即内部的子系统的任何变化不会影响到 | ||
+ | * 相比从单个类的层次,// | ||
+ | * //Façade// 设计模式并非是一个像容器一样的概念,因此我们并不能随意的放进任意对象。 //Façade// 模式中,其内部区域(子系统)应该具有**相互耦合关系比较大**的特性,而不是一个简单的功能集合。 | ||
+ | |||
+ | ===Proxy=== | ||
+ | |||
+ | 在软件的开发过程中,基于某种原因,直接访问有些对象会给使用者或者系统带来很多麻烦(比如创建对象的开销、相关的安全机制、进程之外的访问等等)。这样的情况下,如果我们希望按照原有的方式(透明操作)去访问这个对象,但又希望回避这些“麻烦”,我们就倾向于建立一个 //Proxy// 来作为原有对象和访问者的中间层。通过这个中间层,我们可以对相关的额外内容进行处理(比如添加一些安全性的验证等等),从而实现了在不失去对对象的透明操作的情况下,同时管理和控制这些对象特有的复杂性。 | ||
+ | \\ | ||
+ | \\ | ||
+ | //Proxy// 设计模式的原理很简单,但对于 //Proxy// 对象本身的构建需要针对原始对象所需要的特性来实现。因此,对于不同的原始对象,// | ||
+ | *//Subject Interface// | ||
+ | *//Real Subject// | ||
+ | *//Proxy // | ||
+ | *// | ||
+ | 具体体现到代码中可以表示为: | ||
+ | <code cpp linenums: | ||
+ | class ISubject{ | ||
+ | public: | ||
+ | virtual void process(); | ||
+ | }; | ||
+ | /* Proxy Design */ | ||
+ | class SubjectProxy: | ||
+ | public: | ||
+ | virtual void process(){ | ||
+ | //an Indirection way to call RealSubject | ||
+ | //.... | ||
+ | } | ||
+ | }; | ||
+ | class ClientApp{ | ||
+ | ISubject* subject; | ||
+ | public: | ||
+ | ClientApp() { subject=new SubjectProxy(); | ||
+ | void DoTask(){ | ||
+ | //... | ||
+ | subject-> | ||
+ | //.... | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 可以注意到的是,代理类与真正的类使用了相同的接口。这样的设计也可以从现实社会的角度来看待:真正的当事人由于某些问题,需要授权代理人去代办一些事宜。因此在处理的结果上,代理人与当事人具有同样的效果。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一下 //Proxy// 模式的具体定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * “增加一层间接层” 是软件系统中对很多复杂问题的一种常见解决方法。在面向对象系统中,直接使用某些对象会带来很多问题,作为间接层的 //Proxy// 对象便是解决这一种问题的常用手段。 | ||
+ | * 具体 //Proxy// 设计模式的实现方法和力度根据需求不同相差很大。有些可能对单个对象做非常细的力度控制(比如 // | ||
+ | * //Proxy// 并不一定要求保持接口完整的一致性,只要能够实现间接控制,有时候损失一些透明性是可以接受的。 | ||
+ | |||
+ | ===Adapter=== | ||
+ | |||
+ | //Adapter// 这个概念与生活联系非常紧密。它作为一种旧类型到新类型的转换方式,在生活中随处可见。比如我们在使用显示器的时候,面对显示器和显卡不同的接口,会用到转接口;又譬如我们在使用笔记本的时候,需要将 220V 的标准电压转化为笔记本适用的电压。总的来说,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 而在软件的设计的过程中,我们很可能会遇到接口不一致的情况:某一些现存的对象具有一种接口,而新开发的环境中又有新的接口标准。那么我们如何将这些现存的对象放置到新的环境中使用呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | //Adapter// 设计模式可以通过将旧的类的接口转化为新的接口,从而解决这样的问题。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一看 //Adapter// 具体是如何实现的吧: | ||
+ | \\ | ||
+ | \\ | ||
+ | 首先,// | ||
+ | <code cpp linenums: | ||
+ | //target Interface (new Interface) | ||
+ | class ITarget{ | ||
+ | public: | ||
+ | virtual void process()=0; | ||
+ | }; | ||
+ | //Old Interface | ||
+ | class IAdaptee{ | ||
+ | public: | ||
+ | virtual void foo(int data)=0; | ||
+ | virtual int bar()=0; | ||
+ | }; | ||
+ | //class with old interface | ||
+ | class OldClass: public IAdaptee{ | ||
+ | //.... | ||
+ | }; | ||
+ | class Adapter: public ITarget{ //继承 | ||
+ | .... | ||
+ | }; | ||
+ | </ | ||
+ | 而接下来,// | ||
+ | <code cpp linenums: | ||
+ | class Adapter: public ITarget{ | ||
+ | protected: | ||
+ | IAdaptee* pAdaptee;// | ||
+ | public: | ||
+ | Adapter(IAdaptee* pAdaptee){ this-> | ||
+ | virtual void process(){ | ||
+ | int data=pAdaptee-> | ||
+ | pAdaptee-> | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 上面的 //Adapter// 类通过组合得到了旧接口类型的入口指针,然后通过构造函数初始化使得 //Adapter// 具有了旧接口类的功能,最后通过多态的方式调用了旧接口类下的具体类的实现方法。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 因为 //Adapter// 的转换作用,在新环境下使用旧接口下的具体对象也变得非常简单: | ||
+ | <code cpp linenums: | ||
+ | //a class under old interface | ||
+ | IAdaptee* pAdaptee = new OldClass(); | ||
+ | //use Adapter to fetch the obejct, then adapt it and deliver it with new interface | ||
+ | ITarget* pTarget = new Adapter(pAdaptee); | ||
+ | pTarget-> | ||
+ | </ | ||
+ | 当然,在实际的开发工作中,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 以上的这种 //Adapter// 的类型,被称为**对象适配器**。除此之外,还有一种 //Adapter// 被称为**类适配器**。这种 //Adapter// 通过**多继承**来实现适配:即通过**保护继承**来得到现有对象的实现,同时通过**公有继承**得到新的接口规范。但因为其通过继承获得了旧的具体类的内容,因此在灵活性上远不如**类适配器**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 同时需要指出的是,以上的 //Adapter// 实现只是其中的一种。我们熟知的 //STL// 中的容器 //Stack// 和 //Queue// 也是适配器的一个典型例子,但这两种适配器并没有通过继承来得到新的标准接口,而是内部通过 //Deque// 类型创建了一个对象,然后直接拿来使用。换句话说,这种适配器不但具有适配的功能,本身也是一种具体的实现。就此看来,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 最后来看看 //Adapter// 设计模式的定义: | ||
+ | > | ||
+ | > ——《设计模式》// | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Adapter// 模式主要应用于“希望复用一些现存的类,但是接口由于复用环境要求不一致的情况”,在遗留代码复用和类库迁移等方面非常有用。 | ||
+ | * //GoF 23// 中定义了两种 //Adapter// 模式的实现结构:对象适配器和类适配器。但类适配器采用了**多继承**的实现方式,一般不推荐使用。对象适配器采用了**对象组合**的方式,更符合耦合精神。 | ||
+ | * //Adapter// 模式可以以非常灵活的方式实现,并不用拘泥于 //GoF 23// 中定义的两种结构。比如,我们完全可以将 | ||
+ | |||
+ | ===Mediator=== | ||
+ | |||
+ | 在软件设计中,我们有时候会遇到大量对象相互关联依赖的情况。该情况导致对象之间往往维持一种非常复杂的引用关系,如果遇到一些需求的变更,那么这一系列的引用关系将会面临不断的变化。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 一个典型的例子就是 //GUI// 与内部数据的交互。从功能上来说,我们需要 //GUI// 和内部数据实现同步,即通过 //GUI// 可以操作内部数据,而内部数据的变化也会直接在 //GUI// 上反馈出来。从实现上来说,实现上述功能会要求 //GUI// 和内部数据在关系上相互依赖。然而这样的关系会造成对变化对象的依赖,这是违反了依赖倒置原则的。因此,我们有必要为这种关系提供一个中介层,将 //GUI// 和 内部数据中有关联的对象通过**指定的规范**链接起来。相比起直接相互依赖,这种链接属于**间接**链接;通过稳定该中间层,实现了关联对象的解耦合。我们把这样的设计方法称为 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 中介者模式通过中介对象来管理之前拥有依赖的对象之间的交互,并依赖这样的设计消除对象之间的紧耦合。该方法主要依靠两个步骤来实现: | ||
+ | - 封装变化。 | ||
+ | - 去除对象之间的显式引用(即编译时依赖)。 | ||
+ | 通过以上的更改,中介者模式可以将原有的网状类关系转化为一种星型的类关系,由中介者来承担对象之间的交流业务: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 需要注意的是,我们在进行上述处理时通过了中介者对对象关系进行了解耦合;但同时带来的后果就是将对象交互的负担全交给了中介者。为此,中介者的需要具有相应的控制逻辑来控制对象之间的交流。举个例子,如果我们需要做对象之间的数据交流,我们需要在中介者内部实现对象绑定的协议:比如传递对象的属性,名称,或者制定其他相关的交互协议等等。这一系列的控制逻辑往往使中介者的设计显得更为复杂。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 从具体的实现上来说,中介者模式一共有四种角色: | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | \\ | ||
+ | 从上述的信息可以看出来,中介者模式通过间接的方法,使类关系从网状关系转变到了星状关系,简化了类关系。这样的做法使得类对象之间可以独立变动,不再受其他对象的影响。从这一点上看,中介者模式与门面模式非常相似;唯一的区别只是在于门面模式是处理系统之间的关系,而中介者模式是处理系统内部对象的关系。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一看中介者模式的定义吧: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * // | ||
+ | * 因为其实现机制,// | ||
+ | * //Facade// 模式是用于系统之间(单向)的对象的关联关系解耦;而 // | ||
+ | \\ | ||
+ | \\ |