本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版 | |||
cs:programming:cpp:boolan_cpp:design_pattern_3 [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:boolan_cpp:design_pattern_3 [2024/01/14 13:46] (当前版本) – ↷ 页面programming:cpp:boolan_cpp:design_pattern_3被移动至cs:programming:cpp:boolan_cpp:design_pattern_3 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======C++面向对象高级编程(上)第三周====== | ||
+ | 本页内容是作为 //Boolan// C++ 开发工程师培训系列的笔记。\\ | ||
+ | <wrap em> | ||
+ | ---- | ||
+ | ====设计模式:对象性能==== | ||
+ | |||
+ | 前面学习过的创建型模式大量的使用了抽象的思想来实现类关系之间的低耦合。但在某些情况下(比如大量重复使用),使用面向对象解决问题需要付出一定的代价。因此,我们需要对面向对象所带来的成本进行谨慎处理。对象性能类型的设计模式正是基于这种目的而出现的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 对象性能类型的设计模式主要有两种: | ||
+ | * // | ||
+ | * // | ||
+ | |||
+ | ===Singleton=== | ||
+ | |||
+ | 我们在软件设计的过程中常常会遇到一种特殊的类。这种类需要保证他们的对象在系统里的唯一性;而只有这样做才能确保他们的逻辑正确性以及良好的效率。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 看到这里你可能会想:这多简单,直接告诉使用者我这个类只许创建一个对象不就行了? | ||
+ | \\ | ||
+ | \\ | ||
+ | 实际上,在设计类的过程中,我们需要明确一个态度:作为类的设计者,我们应该主动去承担这份责任,而不是将这份责任转交给使用者。因此,在这个设计模式中,我们需要考虑如何绕过常规的构造函数去提供一种机制,来保证这种累只有一个实例。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 在C++中,这样的实现很简单:只需要将构造函数声明为私有方法就可以了: | ||
+ | <code cpp linenums: | ||
+ | class Singleton{ | ||
+ | private: | ||
+ | Singleton(); | ||
+ | Singleton(const Singleton& | ||
+ | public: | ||
+ | static Singleton* getInstance(); | ||
+ | static Singleton* m_instance; | ||
+ | }; | ||
+ | //declare the object | ||
+ | Singleton* Singleton:: | ||
+ | //if the object is empty, create it | ||
+ | Singleton* Singleton:: | ||
+ | if (m_instance == nullptr) { | ||
+ | m_instance = new Singleton(); | ||
+ | } | ||
+ | return m_instance; | ||
+ | } | ||
+ | </ | ||
+ | 需要注意的是,因为我们将构造函数放入了私有变量,因此该类是不可能形成实例的。因此,我们需要一个静态的方式让其形成实例:'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 这样的写法在逻辑上是没有问题的;进一步说,如果程序是单线程的,那么该实现也是没有问题的。但在多线程的情况下,这段代码会出现潜在的问题。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 问题实际上出现在这一段代码上: | ||
+ | <code cpp linenums: | ||
+ | if (m_instance == nullptr) { | ||
+ | m_instance = new Singleton(); | ||
+ | } | ||
+ | </ | ||
+ | 在多线程的情况下,多个线程可能同时或者在极短的时间差内访问 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 为了解决这个问题,有人提出了使用线程锁的方法。进程锁会在一个线程运行某段代码的时候,让其他线程等候到该线程执行完毕。这样一来,就可以保证 '' | ||
+ | <code cpp linenums: | ||
+ | Singleton* Singleton:: | ||
+ | //adding thread lock | ||
+ | Lock lock; | ||
+ | if (m_instance == nullptr) { | ||
+ | m_instance = new Singleton(); | ||
+ | } | ||
+ | return m_instance; | ||
+ | } | ||
+ | </ | ||
+ | 这样一来,所有其他的线程都必须等到 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 这种实现确实解决了多线程的问题;但又带来了一个新的问题:开销。可以想到的是,线程锁只需要在 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此,第二个方法出现了,也就是著名的**双检查锁**。这种方法通过两个条件来判断是否需要线程锁,即: | ||
+ | * 加锁之前对 '' | ||
+ | * 加锁之后对 '' | ||
+ | 额外的锁前检查确保了只有当执行的操作为初始化对象的时候,才会开启线程锁。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 这样的写法看上去已经没有问题了。而实际上在很长一段时间内,大家都在用这样的写法来写 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 在说明这个问题之前,先要谈一下编译过程中的 //Reorder// 概念。我们在编程的时候,假设的是程序会按我们写的方式按部就班的运行。但在实际的情况中,编译器会根据自身的判断对我们写的代码在指令层级上作出优化;而这样的优化很可能导致我们的程序指令顺序的变化。这样的变化被称为编译过程中的 | ||
+ | \\ | ||
+ | \\ | ||
+ | 以先前代码中的 '' | ||
+ | - 分配内存。 | ||
+ | - 调用构造函数构造对象。 | ||
+ | - 将构造完毕的对象的内存起始地址还给 '' | ||
+ | 然而在实际的编译过程中,这个过程很可能就被替换成了: | ||
+ | - 分配内存 | ||
+ | - 将分配的内存起始地址还给 '' | ||
+ | - 调用构造函数构造对象。 | ||
+ | 如果编译器执行了上述的 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 这样造成的结果就是, '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 为解决这个问题,很多其他的语言提供一个特别的类型(关键字) '' | ||
+ | <code cpp linenums: | ||
+ | std:: | ||
+ | std::mutex Singleton:: | ||
+ | Singleton* Singleton:: | ||
+ | Singleton* tmp = m_instance.load(std:: | ||
+ | std:: | ||
+ | if (tmp == nullptr) { | ||
+ | std:: | ||
+ | tmp = m_instance.load(std:: | ||
+ | if (tmp == nullptr) { | ||
+ | tmp = new Singleton; | ||
+ | std:: | ||
+ | m_instance.store(tmp, | ||
+ | } | ||
+ | } | ||
+ | return tmp; | ||
+ | } | ||
+ | </ | ||
+ | 该方法中通过 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 从以上的内容可以看出来,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 最后来看看 // | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * // | ||
+ | * // | ||
+ | * 双检查锁的正确实现是确保多线程环境下实现 // | ||
+ | |||
+ | ===FlyWeight=== | ||
+ | |||
+ | 在使用面向对象构建软件系统的过程中,我们有时候会遇到这样的情况:一些功能需要大量的细粒度对象来实现。但大量的对象同时带来的是非常高的运行时代价(主要指内存)。使用 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 还是从一个例子谈起。在电脑中的字符都有其对应的字体;但如果按照一对一的方法去构建字体对象,那么很显然这样的运行代价是很高的——一篇十万字的文章就需要十万个字体对象。而我们知道,字符实际上并没有那么多种。也就是说,这十万个字体对象中,有大量的对象是相同的、重复的。我们应该想一个办法来提高这些字体对象的重用性。// | ||
+ | \\ | ||
+ | \\ | ||
+ | // | ||
+ | - 创建一个工厂(字体对象库)。 | ||
+ | - 将字符的种类与字体一对一绑定,创建的时候先按照字体的 '' | ||
+ | - 创建的时候先查询字体库中是否有已存在的 '' | ||
+ | - 否则,创建对象并放置到字体库中。 | ||
+ | 通过查重,我们就可以用多个字符共享一个字体对象,从而达到避免大量细粒度对象的目的了。具体的实现代码如下: | ||
+ | <code cpp linenums: | ||
+ | class Font { | ||
+ | private: | ||
+ | //unique object key | ||
+ | string key; | ||
+ | //object state | ||
+ | //.... | ||
+ | public: | ||
+ | Font(const string& key){ /*...*/} | ||
+ | }; | ||
+ | class FontFactory{ | ||
+ | private: | ||
+ | map< | ||
+ | public: | ||
+ | Font* GetFont(const string& key){ | ||
+ | //check whether the object exists | ||
+ | map< | ||
+ | if(item!=footPool.end()){ | ||
+ | return fontPool[key]; | ||
+ | //create it and add to the lib if no match result | ||
+ | } else{ | ||
+ | Font* font = new Font(key); | ||
+ | fontPool[key]= font; | ||
+ | return font; | ||
+ | } | ||
+ | } | ||
+ | void clear(){ /*...*/ } | ||
+ | }; | ||
+ | </ | ||
+ | 上面的代码只是一段概括性的模型。具体的实现根据需要可能会使用不同的技术和方法来实现,比如不同的数据结构等等。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 而同时需要注意的是,通过上面的代码,我们可以得知一个共享对象很重要的属性:**只读**;这样可以避免一些在共享对象后修改共享对象的操作。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 当然,我们也应该对系统的对象个数进行一个有效的评估,这样才能保证系统的运行代价在可控的范围内。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看看 // | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * 面向对象很好的解决了抽象性的问题。但作为一个运行在机器中的程序实体,我们也需要考虑使用对象带来的代价问题。// | ||
+ | * // | ||
+ | * 对象数量太大会导致对象内存的开销加大。因此我们需要心里有个度:什么样的数量才算大?这个度需要我们仔细的根据具体应用情况来苹果,而不能凭空臆断。 | ||
+ | |||
+ | ====设计模式:状态变化==== | ||
+ | |||
+ | 在组件构建的过程中,某些对象的状态会经常面临变化。为此,我们需要对这些对象变化进行有效的管理;并且,在管理的同时需要维持高层模块的稳定性。状态变化模式为这样的问题提供了一种解决方案。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 典型的状态模式有: | ||
+ | * //State// | ||
+ | * //Memonto// | ||
+ | |||
+ | ===State=== | ||
+ | |||
+ | 在软件的构建过程中,有时候我们会遇到一些经常变化的对象。而这些对象变化的同时,往往其行为也会随之发生变化。比如文档如果处于只读状态,和处于读写状态,支持的操作就有不同的。那么有没有办法避免在更改对象行为的同时,避免造成对象操作与状态转换之间的紧耦合呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一个具体的例子。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 假设我们设计一个网络连接程序,实现一个连接的过程。该程序在连接的过程中有三种状态:'' | ||
+ | <code cpp linenums: | ||
+ | //states | ||
+ | enum NetworkState | ||
+ | { | ||
+ | Network_Open, | ||
+ | Network_Close, | ||
+ | Network_Connect, | ||
+ | }; | ||
+ | // | ||
+ | class NetworkProcessor{ | ||
+ | NetworkState state; | ||
+ | public: | ||
+ | void Operation1(){ | ||
+ | if (state == Network_Open){ | ||
+ | // | ||
+ | state = Network_Close; | ||
+ | } | ||
+ | else if (state == Network_Close){ | ||
+ | // | ||
+ | state = Network_Connect; | ||
+ | } | ||
+ | else if (state == Network_Connect){ | ||
+ | // | ||
+ | state = Network_Open; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | 上面的代码使用了 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 根据 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 明确了思路,那么接下来就是一步一步的修改了: | ||
+ | \\ | ||
+ | \\ | ||
+ | 首先,我们需要实现运行时的状态判断。也就是说,我们要对象操作的方法去自行判断状态。为此,我们需要一个基类来定义这些方法: | ||
+ | <code cpp linenums: | ||
+ | class NetworkState{ | ||
+ | public: | ||
+ | NetworkState* pNext; // denote the next state | ||
+ | virtual void Operation1()=0; | ||
+ | virtual void Operation2()=0; | ||
+ | virtual void Operation3()=0; | ||
+ | virtual ~NetworkState(){} | ||
+ | }; | ||
+ | </ | ||
+ | 接下来,我们通过一个 '' | ||
+ | <code cpp linenums: | ||
+ | class NetworkProcessor{ | ||
+ | NetworkState* pState; | ||
+ | public: | ||
+ | NetworkProcessor(NetworkState* pState){ | ||
+ | // assign concrete state to current object | ||
+ | this-> | ||
+ | } | ||
+ | void Operation1(){ | ||
+ | //... | ||
+ | pState-> | ||
+ | pState = pState-> | ||
+ | //... | ||
+ | } | ||
+ | </ | ||
+ | 这样一来,我们发现操作部分就是稳定的了。操作根本不需要去考虑当前是什么状态,因为所有的操作的关键步骤都是两部:**执行操作**,**转换状态**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 当然,执行完操作,我们需要转换到该操作对应的下一步状态。为此,我们可以通过继承 '' | ||
+ | <code cpp linenums: | ||
+ | class OpenState :public NetworkState{ | ||
+ | static NetworkState* m_instance; | ||
+ | public: | ||
+ | static NetworkState* getInstance(){ | ||
+ | if (m_instance == nullptr) { | ||
+ | m_instance = new OpenState(); | ||
+ | } | ||
+ | return m_instance; | ||
+ | } | ||
+ | void Operation1(){ | ||
+ | // | ||
+ | pNext = CloseState:: | ||
+ | } | ||
+ | void Operation2(){ | ||
+ | // | ||
+ | pNext = ConnectState:: | ||
+ | } | ||
+ | void Operation3(){ | ||
+ | // | ||
+ | pNext = OpenState:: | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 可以看到的是,此处的具体实现中,通过 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 到此,整个改造完毕,我们发现,如果有新的状态加入,我们只需要添加新的状态子类进行相关重写即可。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一下 //State// 模式的定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //State// 模式将所有与一个特定状态相关的行为都放进了一个 //State// 的子类对象中。因此,在对象状态切换的时候,该行为也能进行对应的切换。但同时,维持 //State// 的接口是不变的;这样的方法实现了具体操作和状态转换之间的解耦。 | ||
+ | * 可以看出来的是,对于不同的状态, //State// 模式引入了不同的对象来表示。这样的操作使得状态转换的过程变得更加明确,同时也杜绝了出现状态不一致的情况。 | ||
+ | * 如果 | ||
+ | |||
+ | ===Memonto=== | ||
+ | |||
+ | 在软件的设计过程中,某些对象会不停的发生状态的变化(比如状态对象)。因为某种需要,我们需要将这个状态对象回溯到一个指定的历史记录位置。这样的需求带来了一个问题:如果将这个状态通过公有的接口来让其他对象获取的话,会造成状态对象的封装被破坏,具体的实现细节很可能会被其他的对象获取。那么如何避免这个问题呢? | ||
+ | \\ | ||
+ | \\ | ||
+ | 一个比较不错的实现方法就是:通过一定的方法捕获该状态,再将其保存到对象外部。这样做可以在实现对象的保存和回复的功能下,保证对象本身的封装性。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一下具体的实现: | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们需要实现一个快照功能,该快照功能能够记录对象的历史状态。如果按照上面的实现方法,我们可以将对象状态用另外一个对象来保存: | ||
+ | <code cpp linenums: | ||
+ | /* Memento */ | ||
+ | class Memento | ||
+ | { | ||
+ | string state; | ||
+ | //.. | ||
+ | public: | ||
+ | Memento(const string & s) : state(s) {} | ||
+ | string getState() const { return state; } | ||
+ | void setState(const string & s) { state = s; } | ||
+ | }; | ||
+ | /* state class */ | ||
+ | class Originator | ||
+ | { | ||
+ | string state; | ||
+ | //.... | ||
+ | public: | ||
+ | Originator() {} | ||
+ | Memento createMomento() { | ||
+ | Memento m(state); | ||
+ | return m; | ||
+ | } | ||
+ | void setMomento(const Memento & m) { | ||
+ | state = m.getState(); | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 我们在状态对象中添加了一个 '' | ||
+ | <code cpp linenums: | ||
+ | int main() | ||
+ | { | ||
+ | Originator orginator; | ||
+ | //get current state and save it to memento | ||
+ | Memento mem = orginator.createMomento(); | ||
+ | /*... orginator state has been changed ...*/ | ||
+ | //recover old state from momento | ||
+ | orginator.setMomento(memento); | ||
+ | } | ||
+ | </ | ||
+ | 通过上述的方法,我们就可以在不破坏状态对象封装的条件下实现备忘录功能。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 可能你会觉得,这个方法比较简单,没有必要单独提出来作为一个设计模式。但需要说明的是, | ||
+ | 该设计模式是 //GoF// 在 1994 年提出的。针对于当时的条件,本模式中比较重要的两个过程:保存与回溯,在当年的技术平台下实现起来还是比较困难的,特别是如果我们对快照的数量有要求的情况下。当然,由于程序语言的发展,在现代的编程环境下,我们可以通过一些效率较高的手段(比如序列化,特殊的内存编码)来对这两个步骤0进行处理。一个典型的例子就是快照的捕获:我们需要一个相对于状态对象独立的副本。考虑到对象中可能存在指针,因此我们必须使用深拷贝。而通过序列化方案来实现的话,效率会较高。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 最后来看一下 //Memento// 模式的定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * 备忘录 //Memento// 用于存储状态对象(// | ||
+ | * //Memento// 设计模式的核心是信息隐藏,即 // | ||
+ | * 由于现代语言(// | ||
+ | |||
+ | ====设计模式:数据结构==== | ||
+ | |||
+ | 在软件构建的过程中,我们常常会遇到一些由特定数据结构组成的组件。如果不处理,让客户程序依赖这些特定的数据结构,将会极大的破坏组件的复用性。一个比较好的解决思维是将这些特定的数据解耦股封装在内部,同时在外部提供统一的接口,借此来隔离访问与数据结构。我们把这种思想称为数据结构类型的设计模式。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 该设计模式类型的典型模式有: | ||
+ | * // | ||
+ | * // | ||
+ | * //Chain of Responsibility// | ||
+ | |||
+ | ===Composite=== | ||
+ | |||
+ | 有些时候我们需要处理不同类型的对象,而往往这些对象可能由更简单的对象组合在一起。在设计的时候,我们可能会考虑将简单的对象设计为类,然后使用指针来对其这些类按照一定的数据结构进行组合,从而达到复杂对象的效果。但问题在于,如果我们对这类型的对象都有类似的操作,如何将这个操作独立于这些对象的内部数据结构? | ||
+ | \\ | ||
+ | \\ | ||
+ | 从上述的描述来看,这很显然是一种树型结构。对于这样的对象,我们需要知道它是复杂对象(树)还是简单对象(叶子)。针对这两种情况的不同,处理的方式也不同。 | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 一个典型的例子就是文件夹。文件夹本身可能只会包含基本的文件(叶子),但也可能包含其他的文件夹。因此,对文件夹的操作,往往是像一棵树一样一步一步往下走的;每一个分级实际上都是一个“**部分-整体**”的结构。我们需要对这些文件夹对象进行不同的处理,但对于用户来说,装文件夹的文件夹和装文件的文件夹实际上是一样的,他们期望使用相同的操作来对这些实际上不同的对象进行统一处理。因此,为了达到这样的需求,我们需要首先将接口统一,将具体的操作通过多态去实现: | ||
+ | \\ | ||
+ | \\ | ||
+ | <code cpp linenums: | ||
+ | class Component | ||
+ | { | ||
+ | public: | ||
+ | virtual void process() = 0; | ||
+ | virtual ~Component(){} | ||
+ | }; | ||
+ | </ | ||
+ | 上面的代码实际上就是节点的抽象基类,用户的任意操作都是通过这个抽象基类来实现的。这个基类是稳定的,我们只需要基于这个基类对于不同的节点进行不同的操作就可以: | ||
+ | <code cpp linenums: | ||
+ | /* Leaf */ | ||
+ | class Leaf : public Component{ | ||
+ | string name; | ||
+ | public: | ||
+ | Leaf(string s) : name(s) {} | ||
+ | void process(){ | ||
+ | //process current node | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | //Leaf// 的实现很简单,只需要在子类中重写 '' | ||
+ | - 使用容器存放该对象的所有子节点 | ||
+ | - 遍历该容器,对每个子节点进行 '' | ||
+ | - 如果子节点为简单对象(// | ||
+ | 具体的实现代码如下: | ||
+ | <code cpp linenums: | ||
+ | class Composite : public Component{ | ||
+ | string name; | ||
+ | list< | ||
+ | public: | ||
+ | Composite(const string & s) : name(s) {} | ||
+ | void add(Component* element) { | ||
+ | elements.push_back(element); | ||
+ | } | ||
+ | void remove(Component* element){ | ||
+ | elements.remove(element); | ||
+ | } | ||
+ | void process(){ | ||
+ | //1. process current node | ||
+ | //2. process leaf nodes | ||
+ | for (auto &e : elements) | ||
+ | e-> | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 通过这样的处理方式,我们可以将复杂节点最终分解为简单节点来处理。但这样的处理是内部的,对于用户来说,'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 需要注意的是,对节点的添加或者删除等操作,我们应该放置到 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 最后来看看 // | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * // | ||
+ | * 将客户代码与复杂的对象容器结构解耦是 // | ||
+ | * // | ||
+ | |||
+ | ===Iterator=== | ||
+ | |||
+ | // | ||
+ | \\ | ||
+ | \\ | ||
+ | //GoF// 在 1994 年提出了 // | ||
+ | <code cpp linenums: | ||
+ | /* base */ | ||
+ | template< | ||
+ | class Iterator | ||
+ | { | ||
+ | public: | ||
+ | virtual void first() = 0; | ||
+ | virtual void next() = 0; | ||
+ | virtual bool isDone() const = 0; | ||
+ | virtual T& current() = 0; | ||
+ | }; | ||
+ | /* overwrite */ | ||
+ | template< | ||
+ | class CollectionIterator : public Iterator< | ||
+ | MyCollection< | ||
+ | public: | ||
+ | CollectionIterator(const MyCollection< | ||
+ | void first() override { .... } | ||
+ | void next() override { .... } | ||
+ | bool isDone() const override{ .... } | ||
+ | T& current() override{ .... } | ||
+ | }; | ||
+ | </ | ||
+ | 该思想是非常直观而简单的,即使用多态来解决 // | ||
+ | * 由于模板是编译时多态,面向对象是运行时多态,而 // | ||
+ | * 模板因为其自身特性,相较于面向对象的实现,模板可以实现 // | ||
+ | 当然这只是针对 C++ 来说的。对于一些其他语言(比如 JAVA,C# | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看看 | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * 为什么要将迭代抽象:访问一个聚合对象的内容而无需暴露他的内部表示。 | ||
+ | * 为什么要使用迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。 | ||
+ | * 为什么要考虑迭代器的健壮性:遍历的同时更改迭代器所在的集合结构会导致问题。 | ||
+ | |||
+ | ===Chain of Responsibility=== | ||
+ | |||
+ | 在软件构建过程中,有时候一个请求可能会被多个对象处理,但每个请求在运行的时候只能有一个接受者。如果显式的绑定这两者,则会带来请求发送者和接受者的紧耦合。 | ||
+ | \\ | ||
+ | 我们要如何使请求发送者不需要指定具体的接受者呢? | ||
+ | \\ | ||
+ | 这里我们就要用到 //Chain of Responsibility// | ||
+ | \\ | ||
+ | 具体的实现可以分为好几部分: | ||
+ | \\ | ||
+ | \\ | ||
+ | 请求的类型,通过一个枚举类型来实现。 | ||
+ | < | ||
+ | enum class RequestType | ||
+ | { | ||
+ | REQ_HANDLER1, | ||
+ | REQ_HANDLER2, | ||
+ | REQ_HANDLER3 | ||
+ | }; | ||
+ | </ | ||
+ | 请求处理对象链表的生成。由于具体对请求处理的方法不同,我们需要定义两个方法来判断请求的状态,即请求是否成功和处理请求: | ||
+ | <code cpp linenums: | ||
+ | class ChainHandler{ | ||
+ | | ||
+ | ChainHandler *nextChain; | ||
+ | void sendReqestToNextHandler(const Reqest & req) | ||
+ | { | ||
+ | if (nextChain != nullptr) | ||
+ | nextChain-> | ||
+ | } | ||
+ | protected: | ||
+ | virtual bool canHandleRequest(const Reqest & req) = 0; | ||
+ | virtual void processRequest(const Reqest & req) = 0; | ||
+ | public: | ||
+ | ChainHandler() { nextChain = nullptr; } | ||
+ | void setNextChain(ChainHandler *next) { nextChain = next; } | ||
+ | void handle(const Reqest & req) | ||
+ | { | ||
+ | if (canHandleRequest(req)) | ||
+ | processRequest(req); | ||
+ | else | ||
+ | sendReqestToNextHandler(req); | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 该实现中,'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 剩余的部分就是对先前说的两个主要方法进行重写了。这些子类的重写需要根据请求类型的不同来进行重写: | ||
+ | <code cpp linenums: | ||
+ | class Handler1 : public ChainHandler{ | ||
+ | protected: | ||
+ | bool canHandleRequest(const Reqest & req) override | ||
+ | { | ||
+ | return req.getReqType() == RequestType:: | ||
+ | } | ||
+ | void processRequest(const Reqest & req) override | ||
+ | { | ||
+ | cout << " | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 通过以上的操作,我们发现只要将请求放入这个链表就可以了;处理对象的内容会依次被请求浏览,而处理对象内容的不同则通过运行时的多态来处理。这样既保证了请求可以与具体处理对象互动,同时又避免了将请求与对象绑定在一起造成紧耦合。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看看 //Chain of Responsibility// | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Chain of Responsibility// | ||
+ | * 当我们应用了 //Chain of Responsibility// | ||
+ | * 如果请求传递到职责链的末尾仍然得不到处理,我们应该有一个合理的缺省机制。这也是每一个接受对象的责任,而不是发送请求者的责任。 | ||
+ | |||
+ | ====设计模式:行为变化==== | ||
+ | |||
+ | 在组件的构建中,组件的行为变黄经常导致组件本身剧烈的变化。在某些情况下,我们需要对组件的行为和组件本身进行解耦,以达到支持组件行为的变化,实现两者之间的松耦合的目的。这些工作就是行为变化模式需要处理的主要问题。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 典型的行为变化模式有: | ||
+ | * //Command// | ||
+ | * //Visitor// | ||
+ | |||
+ | ===Command=== | ||
+ | |||
+ | 在软件的构建过程中,行为请求者与行为实现者通常呈现了一种紧耦合。比如类对象中的方法,是天然的,与类本身编译时绑定的。但在某些场合——比如需要对行为进行 “记录、撤销、重做、事务” 等等处理,那么这种紧耦合就出出问题了。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 比如我们去商场买东西,每个人都有自己的需求:比如买东西,退东西,讨价还价等等。如果大家都在那里按自身的意愿提出要求的化,那么很可能就乱套了;商家根本记不过来这么多信息,很可能就会出错;而且同一个人的请求,他可能会提好多遍,这对于商家处理人群的要求来说,更是种负担。如果将这些请求都转化为订单呢?是不是就清楚多了,商家可以按照订单的顺序给大家供应实物,一个人只需要发出一次请求,就可以得到相应的饭菜了。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 按我的理解,这才是为什么 //Command// 模式非要将行为抽象为对象原因;因为这样实在是太方便了。客户根本不用直接去前台咋呼,也不用管谁在做自己的饭,只需要提交一个订单,在一旁等就可以了。这不就是行为请求者和行为实现者之间的松耦合吗? | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看下具体的实现: | ||
+ | <code cpp linenums: | ||
+ | class Command | ||
+ | { | ||
+ | public: | ||
+ | virtual void execute() = 0; | ||
+ | }; | ||
+ | |||
+ | class ConcreteCommand1 : public Command | ||
+ | { | ||
+ | string arg; | ||
+ | public: | ||
+ | ConcreteCommand1(const string & a) : arg(a) {} | ||
+ | void execute() override { cout<< | ||
+ | }; | ||
+ | class ConcreteCommand2 : public Command | ||
+ | { | ||
+ | string arg; | ||
+ | public: | ||
+ | ConcreteCommand2(const string & a) : arg(a) {} | ||
+ | void execute() override { cout<< | ||
+ | }; | ||
+ | </ | ||
+ | 通过上述代码的实现,我们发现,对行为和对象本身进行解耦真是好处多多,提高了效率,还可以自定义方法,比起以前只要提交请求就要去使用对象本身来说要好的多了。同时可以看出来的是,上面的代码的核心就是 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看看 //Command// 模式的定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Command// 模式的根本目的在于将**行为请求者**和**行为实现者**解耦。而解耦在面向对象中常用的手段则是将**行为抽象为对象**。 | ||
+ | * 实现 | ||
+ | * //Command// 模式与C++中的函数对象有些类似,但两者定义行为接口的规范有所区别:// | ||
+ | |||
+ | ===Visitor=== | ||
+ | |||
+ | 在软件的构建过程中,我们常常会在某些层次结构中添加新的**功能**(方法)。遇到这样的问题,大家可能第一时间就会想到去基类中添加该功能,然后到对应的子类里去添加该功能的重写。不过这样做的话,工作量是相当大的,我们需要挨个去维护每个子类。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 有没有一种方法在不改变类层次结构的前提下,在运行时的时候根据需要动态的添加新的操作,从而避免上述的问题? | ||
+ | \\ | ||
+ | \\ | ||
+ | 还是从代码入手。假设我们有一个基类 '' | ||
+ | <code cpp linenums: | ||
+ | class Element | ||
+ | { | ||
+ | public: | ||
+ | virtual void Func1() = 0; | ||
+ | virtual void Func2(int data)=0; | ||
+ | virtual ~Element(){} | ||
+ | }; | ||
+ | class ElementA : public Element | ||
+ | { | ||
+ | public: | ||
+ | void Func1() override{ /*....*/ } | ||
+ | void Func2(int data) override{ /*....*/ } | ||
+ | }; | ||
+ | class ElementB : public Element | ||
+ | { | ||
+ | public: | ||
+ | void Func1() override{ /*....*/ } | ||
+ | void Func2(int data) override{ /*....*/ } | ||
+ | }; | ||
+ | </ | ||
+ | 如果我们需要添加一个新的功能,比如 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | //Visitor// 设计模式给出了一个非常好的答案。来看看 //Visitor// 设计模式是如何将功能独立出来添加到已存在的结构中的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 首先,我们已有的结构,就是需要添加功能的对象,如上面的 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此,我们需要有另外一个**方法基类**,用于定义我们需要新添加进去的功能: | ||
+ | <code cpp linenums: | ||
+ | class Visitor{ | ||
+ | public: | ||
+ | virtual void visitElementA(ElementA& | ||
+ | virtual void visitElementB(ElementB& | ||
+ | virtual ~Visitor(){} | ||
+ | }; | ||
+ | </ | ||
+ | // | ||
+ | \\ | ||
+ | \\ | ||
+ | 上面的方法都接收一个具体的 '' | ||
+ | <code cpp linenums: | ||
+ | class Visitor1 : public Visitor{ | ||
+ | public: | ||
+ | void visitElementA(ElementA& | ||
+ | cout << " | ||
+ | } | ||
+ | void visitElementB(ElementB& | ||
+ | cout << " | ||
+ | } | ||
+ | }; | ||
+ | class Visitor2 : public Visitor{ | ||
+ | public: | ||
+ | void visitElementA(ElementA& | ||
+ | cout << " | ||
+ | } | ||
+ | void visitElementB(ElementB& | ||
+ | cout << " | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 现在我们将需要添加的功能已经做好了,我们需要将这些新功能添加到原有的结构中。因此,在原有的结构中必须**预留好接收新功能的接口**: | ||
+ | <code cpp linenums: | ||
+ | lass Element | ||
+ | { | ||
+ | public: | ||
+ | virtual void accept(Visitor& | ||
+ | virtual ~Element(){} | ||
+ | }; | ||
+ | class ElementA : public Element | ||
+ | { | ||
+ | public: | ||
+ | void accept(Visitor & | ||
+ | visitor.visitElementA(*this); | ||
+ | } | ||
+ | }; | ||
+ | class ElementB : public Element | ||
+ | { | ||
+ | public: | ||
+ | void accept(Visitor & | ||
+ | visitor.visitElementB(*this); | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 来看一看以上的代码是如何使用 '' | ||
+ | <code cpp linenums: | ||
+ | Visitor2 visitor; | ||
+ | ElementB elementB; | ||
+ | elementB.accept(visitor);// | ||
+ | </ | ||
+ | 根据上面的应用,我们可以大致明白这个添加的流程: | ||
+ | - '' | ||
+ | - 当有新扩展方法的时候,'' | ||
+ | - 指定的类通过重写 '' | ||
+ | 可以看出来的是,上述例子中 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 上述的这种过程,我们称之为 //Double Dispatch// ,是 //Visitor// 模式非常典型的特征。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 到此, //Visitor// 模式的大致实现已经完毕了。我们可以看到,通过 //Visitor// 模式,新增加的方法可以独立于旧结构之外作为扩展部分进行添加。同时通过两次运行时多态的方式,使得指定对象可以准确的调用相应的新增方法。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 不过即便如此,// | ||
+ | <code cpp linenums: | ||
+ | virtual void accept(Visitor& | ||
+ | </ | ||
+ | 也就是说,在使用 //Visitor// 模式之前,我们必须**预知该结构一定会添加新的方法**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 不仅如此,我们再来看看 //Visitor// 类中有什么: | ||
+ | <code cpp linenums: | ||
+ | virtual void visitElementA(ElementA& | ||
+ | virtual void visitElementB(ElementB& | ||
+ | </ | ||
+ | 假设我们要加一个 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此,相信你也看出来了,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 最后来看看 //Visitor// 设计模式的定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * //Visitor// 模式通过所谓双重分发(// | ||
+ | * 所谓的双重分发即 | ||
+ | * //Visitor// 的最大缺点在于当扩展类层次结构(添加新的 //Element// 子类)时,会导致 //Visitor// 类的改变。因此, //Visitor// 模式适用于 //Element// 类层次结构稳定,而其中的**操作**会频繁改动的情况。 | ||
+ | |||
+ | ====设计模式:领域规则==== | ||
+ | |||
+ | 在特定的领域中,某些变化虽然频繁,但可以抽象为某种规则。我们可以在这种情况下结合特定领域,将问题抽象为语法规则,从而给出在领域下的一般性解决方案。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 领域规则类型的典型设计模式有: | ||
+ | * // | ||
+ | |||
+ | ===Interpreter=== | ||
+ | |||
+ | 我们在软件构建过程中可能会遇到某种属于特定领域的问题。这样的问题比较复杂,但在其内部有类型的结构不断的重复出现。如果按照普通的处理方法实现起来非常复杂。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 这种情况下,我们可以将特定领域的问题表达为某种语法规则下的句子,然后构建一个解释器来解释这样的句子,从而达到解决问题的目的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看一个具体的例子:比如我们要实现一个多个数的复合加减法式子。如果按照一般的思维方式去想,这个是很难抽象的。因为牵涉到的运算对象个数会不同,运算符的数量和类型也都会不同。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们不妨换一种思维来思考这个问题。举个例子:'' | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 也就是说,上述的长表达式可以递归的写成下一级的运算结果与本级的元素进行运算,而式子最后的结果也就是递归完毕之后所有结果的和。这就是抽象出来的规律;而按照这样的规律,我们可以将参与运算的基本元素设计为两个部分:变量表达式和符号表达式。根据面向对象的设计原则,我们需要有一个统一的抽象类来表示这些元素,然后在子类中去实现细节。而考虑到式子中有运算符,因此我们需要添加一个 '' | ||
+ | <code cpp linenums: | ||
+ | class Expression { | ||
+ | public: | ||
+ | virtual int interpreter(map< | ||
+ | virtual ~Expression(){} | ||
+ | }; | ||
+ | |||
+ | class VarExpression: | ||
+ | char key; | ||
+ | public: | ||
+ | VarExpression(const char& key) { this-> | ||
+ | int interpreter(map< | ||
+ | }; | ||
+ | class SymbolExpression : public Expression { | ||
+ | // 运算符左右两个参数 | ||
+ | protected: | ||
+ | Expression* left; | ||
+ | Expression* right; | ||
+ | public: | ||
+ | SymbolExpression( Expression* left, Expression* right): | ||
+ | left(left), | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 有了基本的元素以后,接下来我们可以通过总结的规律开始制定规则。首先我们的符号有 '' | ||
+ | <code cpp linenums: | ||
+ | // | ||
+ | class AddExpression : public SymbolExpression { | ||
+ | public: | ||
+ | AddExpression(Expression* left, Expression* right): | ||
+ | SymbolExpression(left, | ||
+ | } | ||
+ | int interpreter(map< | ||
+ | return left-> | ||
+ | } | ||
+ | }; | ||
+ | // | ||
+ | class SubExpression : public SymbolExpression { | ||
+ | public: | ||
+ | SubExpression(Expression* left, Expression* right): | ||
+ | SymbolExpression(left, | ||
+ | } | ||
+ | int interpreter(map< | ||
+ | return left-> | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 以上的两个为具体的运算实现,通过 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 现在我们拥有了变量类,运算符类。接下来我们需要让计算机明白该式子的运算规则。按照先前我们发现的规则,我们需要用一种方式让计算机将该式子分解为简单的运算。为此,我们需要用到两个方法: | ||
+ | * 递归 | ||
+ | * //Stack// 数据结构 | ||
+ | 具体的思路可以表现为: | ||
+ | - 读入当前字符 | ||
+ | - 通过条件判断当前字符类型 | ||
+ | - 如果是变量表达式,则存入 //Stack// | ||
+ | - 如果是运算表达式,表示此时需要运算。此时将栈顶元素(// | ||
+ | - 当字符串读取完毕,栈顶元素则为最后的和。 | ||
+ | 具体代码如下: | ||
+ | <code cpp linenums: | ||
+ | Expression* | ||
+ | stack< | ||
+ | Expression* left = nullptr; | ||
+ | Expression* right = nullptr; | ||
+ | for(int i=0; i< | ||
+ | { | ||
+ | switch(expStr[i]) | ||
+ | { | ||
+ | case ' | ||
+ | // 加法运算 | ||
+ | left = expStack.top(); | ||
+ | right = new VarExpression(expStr[++i]); | ||
+ | expStack.push(new AddExpression(left, | ||
+ | break; | ||
+ | case ' | ||
+ | // 减法运算 | ||
+ | left = expStack.top(); | ||
+ | right = new VarExpression(expStr[++i]); | ||
+ | expStack.push(new SubExpression(left, | ||
+ | break; | ||
+ | default: | ||
+ | // 变量表达式 | ||
+ | expStack.push(new VarExpression(expStr[i])); | ||
+ | } | ||
+ | } | ||
+ | Expression* expression = expStack.top(); | ||
+ | return expression; | ||
+ | } | ||
+ | </ | ||
+ | 实际使用中,只要给出符合格式的式子,以及各变量的初值,就可以进行计算了: | ||
+ | <code cpp linenums: | ||
+ | string expStr = " | ||
+ | map< | ||
+ | var.insert(make_pair(' | ||
+ | var.insert(make_pair(' | ||
+ | var.insert(make_pair(' | ||
+ | var.insert(make_pair(' | ||
+ | var.insert(make_pair(' | ||
+ | Expression* expression= analyse(expStr); | ||
+ | </ | ||
+ | 我们可以看见该实现通过虚函数为基础的递归解析,将一个复杂的式子转化成了有一定规律的重复简单运算。这就是解释器模式所达到的效果。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 需要注意的是,解释器模式适合比较简单的规则,对于较复杂的表达式或者规律很难掌握的表达式可能需要寻求其他办法。另外,解释器模式必须要做好良好的内存管理。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来看看解释器模式的定义: | ||
+ | > | ||
+ | > | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 总结: | ||
+ | * // | ||
+ | * 使用 // | ||
+ | * // | ||
+ | \\ | ||
+ | \\ |