2015年11月

原型模式

概述

我们都知道,创建型模式一般是用来创建一个新的对象,然后我们使用这个对象完成一些对象的操作。我们通过原型模式可以快速的创建一个对象,而不需要提供专门的构造函数,特别对于对象构造非常复杂的类来说,这无疑是一种非常有效的方式。

例子:
下面是一个邮寄快递的场景:
“给我寄个快递。”顾客说。
“寄往什么地方?”你问。
“和上次差不多一样,只是邮寄给另外一个地址,这里是邮寄地址……”顾客一边说一边把写有邮寄地址的纸条给你。
“好!”你愉快地答应,因为你保存了用户的以前邮寄信息,只要复制这些数据,然后通过简单的修改就可以快速地创建新的快递数据了。

问题

当对象的构造函数非常复杂,生成新对象非常耗时间、耗资源时,怎么来快速创建新对象呢?

解决方案

原型模式:通过复制(克隆、拷贝)一个指定类型的对象来创建更多同类型的对象。这个指定的对象可被称为“原型”对象。

适用性

原型模式的主要思想是基于现有的对象克隆一个新的对象出来,一般调用原型对象的内部提供克隆的方法,通过该方法返回一个对象的副本。这种创建对象的方式,相比我们之前说的几类创建型模式还是有区别的,之前的讲述的工厂模式与抽象工厂都是通过工厂封装具体的new操作的过程,返回一个新的对象,有的时候我们通过这样的创建工厂创建对象不值得,特别是以下的几个场景的时候,可能使用原型模式更简单也效率更高。

  1. 当一个系统应该独立于它的产品创建、构成和表示时,要使用 Prototype模式
  2. 当要实例化的类是在运行时刻指定时,例如,通过动态装载;
  3. 为了避免创建一个与产品类层次平行的工厂类层次时
  4. 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。(也就是当我们在处理一些对象比较简单,并且对象之间的区别很小,可能只是很固定的几个属性不同的时候,可能我们使用原型模式更合适)。

结构

原型模式结构如图所示:
prototype

组成

  1. 客户(Client)角色:使用原型对象的客户程序
  2. 抽象原型(Prototype)角色:规定了具体原型对象必须实现的接口(如果要提供深拷贝,则必须具有实现clone的规定)
  3. 具体原型(ConcretePrototype):从抽象原型派生而来,是客户程序使用的对象,即被复制的对象。此角色需要实现抽象原型角色所要求的接口。

效果

Prototype模式有许多和Abstract Factory模式 和 Builder模式一样的效果:它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目。此外,这些模式使客户无需改变即可使用与特定应用相关的类。
下面列出Prototype模式的另外一些优点。

  1. 运行时刻增加和删除产品: Prototype允许只通过客户注册原型实例就可以将一个新的具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时刻建立和删除原型。
  2. 改变值以指定新对象: 高度动态的系统允许你通过对象复合定义新的行为—例如,通过为一个对象变量指定值—并且不定义新的类。你通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。这种设计使得用户无需编程即可定义新“类” 。实际上,克隆一个原型类似于实例化一个类。Prototype模式可以极大的减少系统所需要的类的数目。
  3. 改变结构以指定新对象:许多应用由部件和子部件来创建对象。
  4. 减少子类的构造 Factory Method 经常产生一个与产品类层次平行的 Creator类层次。Prototype模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象。因此你根本不需要Creator类层次。这一优点主要适用于像 C + +这样不将类作为一级类对象的语言。像Smalltalk和Objective C这样的语言从中获益较少,因为你总是可以用一个类对象作为生成者。在这些语言中,类对象已经起到原型一样的作用了。
  5. 用类动态配置应用 一些运行时刻环境允许你动态将类装载到应用中。在像 C + +这样的语言中,Prototype模式是利用这种功能的关键。一个希望创建动态载入类的实例的应用不能静态引用类的构造器。而应该由运行环境在载入时自动创建每个类的实例,并用原型管理器来注册这个实例(参见实现一节) 。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。 ET++应用框架[WGM88]有一个运行系统就是使用这一方案的。

Prototype的主要缺陷是每一个Prototype的子类都必须实现clone操作,这可能很困难。
例如,当所考虑的类已经存在时就难以新增 clone操作。当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难的。

实现

cpp

带Prototype Manager的原型模式

原型模式的第二种形式是带原型管理器的原型模式,其UML图如下:
prototype_manager

原型管理器(Prototype Manager)角色:创建具体原型类的对象,并记录每一个被创建的对象

下面这个例子演示了在原型管理器中存储用户预先定义的颜色原型,客户通过原型管理器克隆颜色对象。

red = $red; $this->green = $green; $this->blue = $red; } /** * set red * * @param unknown_type $red */ public function setRed($red) { $this->red = $red; } /** * get red * */ public function getRed(){ return $this->red; } /** *set Green * * @param $green */ public function setGreen($green) { $this->green = $green; } /** * get Green * * @return unknown */ public function getGreen() { return $this->green ; } /** *set Blue * * @param $Blue */ public function setBlue($Blue) { $this->blue = $Blue; } /** * get Blue * * @return unknown */ public function getBlue() { return $this->blue ; } /** * Enter description here... * * @return unknown */ function copy(){ return clone $this; } function display() { echo $this->red , ',', $this->green, ',', $this->blue ,'
'; } } /** * Enter description here... * */ class ColorManager { // Fields static $colors = array(); // Indexers public static function add($name, $value){ self::$colors[$name] = $value; } public static function getCopy($name) { return self::$colors[$name]->copy(); } } /** *Client * */ class Client { public static function Main() { //原型:白色 ColorManager::add("white", new Color( 255, 0, 0 )); //红色可以由原型白色对象得到,只是重新修改白色: r $red = ColorManager::getCopy('white'); $red->setRed(255); $red->display(); //绿色可以由原型白色对象得到,只是重新修改白色: g $green = ColorManager::getCopy('white'); $green->setGreen(255); $green->display(); //绿色可以由原型白色对象得到,只是重新修改白色: b $Blue = ColorManager::getCopy('white'); $Blue->setBlue(255); $Blue->display(); } } ini_set('display_errors', 'On'); error_reporting(E_ALL & ~ E_DEPRECATED); Client::Main(); ?>

代理模式

概述

你曾有过因为某个对象消耗太多资源,而且你的代码并不是每个逻辑路径 ( if和else就是不同的两条逻辑路径)都需要此对象, 而延迟创建对象的想法吗 ?

你有想过根据对象属性限制访问某个对象,比如提供一组方法给普通用户,特别方法给管理员用户?

以上两种需求都非常类似,并且都需要解决一个更大的问题:你如何提供一致的接口给某个对象让它可以改变其内部功能,或者是从来不存在的功能?

可以通过引入一个新的对象,来实现对真实对象的操作或者将新的对象作为真实对象的一个替身。即代理对象。它可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。

例子1:经典例子就是网络代理,你想访问facebook或者twitter,找个代理网站,绕过GFW

问题:

你怎样才能在不直接操作对象的情况下,对此对象进行访问?

解决方案

代理模式: 为其他对象提供一种代理,并以控制对这个对象的访问。而对一个对象进行访问控制的一个原因是为了只有在我们确实需要这个对象时才对它进行创建和初始化。它是给某一个对象提供一个替代者(占位者),使之在client对象和subject对象之间编码更有效率。代理可以提供延迟实例化(lazy instantiation),控制访问等等,包括只在调用中传递。一个处理纯本地资源的代理有时被称作虚拟代理。远程服务的代理常常称为远程代理。强制控制访问的代理称为保护代理。

实用性

在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用Proxy模式。下面是一些可以使用Proxy模式常见情况:

  1. 远程代理(Remote Proxy)为一个位于不同的地址空间的对象提供一个本地的代理对象。这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)
  2. 虚拟代理(Virtual Proxy)根据需要创建开销很大的对象。如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
  3. 保护代理(Protection Proxy)控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。
  4. 智能指引(Smart Reference)取代了简单的指针,它在访问对象时执行一些附加操作。
  5. Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。

结构

Uml图:
proxy

简单结构示意图:
shiyitu

代理类和实现类都继承自同一个父类,这个父类定义了被代理类对外的接口,代理类包含实现类作为成员属性

模式的组成

代理角色(Proxy):

  • 保存一个引用使得代理可以访问实体。若 RealSubject和Subject的接口相同,Proxy会引用Subject。
  • 提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体。
  • 控制对实体的存取,并可能负责创建和删除它。
  • 其他功能依赖于代理的类型:
  • Remote Proxy负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求。
  • Virtual Proxy可以缓存实体的附加信息,以便延迟对它的访问。
  • Protection Proxy检查调用者是否具有实现一个请求所必需的访问权限。

抽象主题角色(Subject)

定义真实主题角色RealSubject和抽象主题角色Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy。代理主题通过持有真实主题RealSubject的引用,不但可以控制真实主题RealSubject的创建或删除,可以在真实主题RealSubject被调用前进行拦截,或在调用后进行某些操作.

真实主题角色(RealSubject)

定义了代理角色(proxy)所代表的具体对象.

效果

Proxy模式在访问对象时引入了一定程度的间接性。根据代理的类型,附加的间接性有多种用途:

  1. Remote Proxy可以隐藏一个对象存在于不同地址空间的事实。也使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  2. Virtual Proxy 可以进行最优化,例如根据要求创建对象。即通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗。
  3. Protection Proxies和Smart Reference都允许在访问一个对象时有一些附加的内务处理(Housekeeping task) 。

Proxy模式还可以对用户隐藏另一种称之为写时复制(copy-on-write)的优化方式,该优化与根据需要创建对象有关。拷贝一个庞大而复杂的对象是一种开销很大的操作,如果这个拷贝根本没有被修改,那么这些开销就没有必要。用代理延迟这一拷贝过程,我们可以保证只有当这个对象被修改的时候才对它进行拷贝。在实现copy-on-write时必须对实体进行引用计数。拷贝代理仅会增加引用计数。只有当用户请求一个修改该实体的操作时,代理才会真正的拷贝它。在这种情况下,代理还必须减少实体的引用计数。当引用的数目为零时,这个实体将被删除。copy-on-write可以大幅度的降低拷贝庞大实体时的开销。

代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。

代理模式的缺点
由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

实现

我们用获取天气预报的例子说明代理模式:

<?php  
/**  
* 代理模式  
*  
* 为其他对象提供一个代理以控制这个对象的访问  
*  
*/   
/** 
 *  抽象主题角色(Subject):天气 
 * 
 */  
interface Weather  
{  
    public function request($city);  
    public function display($city);  
    public function isValidCity($city);  

}  

/** 
 * 真实主题角色(RealSubject): 
 * 
 */  
class RealWeather implements Weather   
{  
    protected $_url = 'http://www.google.com/ig/api?&oe=utf-8&hl=zh-cn&weather=';  
    protected $_weatherXml = '' ;  
    function __construct(){  

    }  

    public function request($city){  
        $this->_weatherXml = file_get_contents($this->_url . $city );  
    }  
    public function display($city ){  
        if ($this->_weatherXml == '') {  
            $this->request($city);  
        }  
        //$this->_weatherXml = mb_convert_encoding($this->_weatherXml, 'UTF-8', 'GB2312');  
        $weatherxml = simplexml_load_string($this->_weatherXml);  
        $low = intval($weatherxml->weather->forecast_conditions[0]->low->attributes());  
        $high = $weatherxml->weather->forecast_conditions[0]->high->attributes();  
        $icon= 'http://www.google.com'. $weatherxml->weather->forecast_conditions[0]->icon->attributes();  
        $condition=$weatherxml->weather->forecast_conditions[0]->condition->attributes();  
        $weather = date('Y年n月j日').'  天气预报:<span class="cor_ff6c00 f_bold">'.$city_names[$city].' </span>  <img class="v_middle" src="'.$icon.'" alt="'.$condition.'" width="16" height="17" align="absmiddle" /> <span class="f_bold"></span>:    '.$low.'°C ~ '.$high.'°C '.$condition;  
        echo  $weather;  
    }  

    public function isValidCity($city){  

    }  

}  

/** 
 * 代理角色(Proxy):延迟代理  
 * 
 */  
class ProxyWeather  implements Weather {  
    private $_client ;  
    private function client() {  
        if (! $this->_client instanceof RealWeather) {  
            $this->_client = new RealWeather();  
        }  
        return $this->_client;  

    }  
    public function request($city){  
        $this->_client()->request($city);  
    }  

    public function isValidCity($city) {  
        return $this->_client()->isValidCity($city);  
    }  


    public function display($city) {  
        return $this->client()->display($city);  
    }  
}  
/** 
 * 代理角色(Proxy):动态代理 
 * 
 */  
class GenericProxyWeather {  

    protected $_subject;  
    public function __construct($subject) {  
        $this->_subject = $subject;  
    }  

    public function __call($method, $args) {  
        return call_user_func_array(  
        array($this->_subject, $method),  
        $args);  
    }  

}  



class Client{  

    static function main(){  
        $proxy = new ProxyWeather();  
        $report = $proxy->display('beijing');  
    }  
    static function Genericmain(){  
        $proxy = new GenericProxyWeather(new RealWeather());  
        $report = $proxy->display('beijing');  
    }  
}  
header('Content-type:text/html;charset=UTF-8');  
Client::main();  

与其他相关模式

  1. 适配器模式Adapter:适配器Adaptor为它所适配的对象提供了一个不同的接口。相反,代理提供了与它的实体相同的接口。然而,用于访问保护的代理可能会拒绝执行实体会执行的操作,因此,它的接口实际上可能只是实体接口的一个子集。
  2. 装饰器模式Decorator:尽管Decorator的实现部分与代理相似,但Decoator的目的不一样。Decorator为对象添加一个或多个功能,而代理则控制对对象的访问。

总结

代理模式在很多情况下都非常有用,特别是你想强行控制一个对象的时候,比如延迟加载,监视状态变更的方法等等

  1. “增加一层间接层”是软件系统中对许多负责问题的一种常见解决方法。在面向对象系统中,直接使用某些对象会带来很多问题,作为间接层的proxy对象便是解决这一问题的常用手段。

  2. 具体proxy设计模式的实现方法、实现粒度都相差很大,有些可能对单个对象作细粒度的控制,有些可能对组件模块提供抽象代理层,在架构层次对对象作proxy。

  3. proxy并不一定要求保持接口的一致性,只要能够实现间接控制,有时候损及一些透明性是可以接受的。例如上面的那个例子,代理类型ProxyClass和被代理类型LongDistanceClass可以不用继承自同一个接口,正像GoF《设计模式》中说的:为其他对象提供一种代理以控制这个对象的访问。代理类型从某种角度上讲也可以起到控制被代理类型的访问的作用。

适配器模式

概述

接口的改变,是一个需要程序员们必须(虽然很不情愿)接受和处理的普遍问题。程序提供者们修改他们的代码,系统库被修正,各种程序语言以及相关库的发展和进化。

例子1:iphone4,你即可以使用UBS接口连接电脑来充电,假如只有iphone没有电脑,怎么办呢?苹果提供了iphone电源适配器。可以使用这个电源适配器充电。这个iphone的电源适配器就是类似我们说的适配器模式。(电源适配器就是把电源变成需要的电压,也就是适配器的作用是使得一个东西适合另外一个东西。)

例子2:最典型的例子就是很多功能手机,每一种机型都自带有从电器,有一天自带充电器坏了,而且市场没有这类型充电器可买了。怎么办?万能充电器就可以解决。这个万能充电器就是适配器。

问题

你如何避免因外部库的API改变而带来的不便?
例如,你写了一个Target类提供以下API

class Target
{
public:
    void hello(){}
    void world(){}
};

client调用了这些API

Target target();  
target.hello();  
target.world();

之后Target类更新了,一个全新的greet()方法代替了hello()。这时候client调用就会出错

class Target
{
public:
    void greet(){}
    void world(){}
};

这时候你能否提供一种方法让client尽量少修改代码,或者不用修改完美地升级呢?

再考虑一种情况,作为client调用第三方API,如果试验新版本API库所带来的特性,但是又不想改动旧的程序。这时候你会怎么做?

解决方案

适配器(Adapter)模式可以把一个类的接口(Adaptee)通过适配器(Adapter)变换成客户端所期待的另一种接口(Target), 使原本因接口不匹配(或者不兼容)而无法在一起工作的两个类能够在一起工作,避免了因升级和拆解客户代码所引起的纠纷。

分类

共有两类适配器模式:

  1. 类适配器模式
    采用继承实现,适配器继承自已实现的类(一般多重继承)。Adapter与Adaptee是继承关系

  2. 对象适配器模式
    采用对象组合方式实现,适配器容纳Adaptee类的实例。Adapter与Adaptee是委托关系

无论哪种适配器,它的宗旨都是:保留现有类所提供的服务,向客户提供接口,以满足客户的期望。,即在不改变原有系统的基础上,提供新的接口服务。

适用性

以下情况使用Adapter模式:

  1. 你想使用一个已经存在的类,而它的接口不符合你的需求。
  2. 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
  3. (仅适用于对象Adapter)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。即仅仅引入一个对象,并不需要额外的指针以间接取得adaptee。

结构

类适配器使用多重继承对一个接口与另一个接口进行匹配,如下图所示:
adapter

对象匹配器依赖于对象组合,如下图所示:
adapter

适配器模式的组成

  • 目标角色(Target):— 定义Client使用的与特定领域相关的接口。
  • 客户角色(Client):与符合Target接口的对象协同。
  • 被适配橘色(Adaptee):定义一个已经存在并已经使用的接口,这个接口需要适配。
  • 适配器角色(Adaptor) :适配器模式的核心。它将对被适配Adaptee角色已有的接口转换为目标角色Target匹配的接口。对Adaptee的接口与Target接口进行适配.

效果

类适配器和对象适配器有不同的权衡。

类适配器

  1. 用一个具体的Adapter类和Target进行匹配,但是是Adaptor不能匹配一个类以及所有它的子类时
  2. 使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子集
  3. 仅仅引入一个对象,并不需要额外的指针以间接取得Adaptee

对象适配器

  1. 允许一个Adapter与多个Adaptee同时工作。Adapter也可以一次给所有的Adaptee添加功能
  2. 使用重定义Adaptee的行为比较困难

使用Adapter模式时需要考虑的其他因素

  1. Adapter的匹配程度
    对Adaptee的接口与Target的接口进行匹配的工作量各个Adapter可能不一样。工作范围可能是,从简单的接口转换(例如改变操作名 )到支持完全不同的操作集合。Adapter的工作量取决于Target接口与Adaptee接口的相似程度

  2. 可插入的Adapter
    当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具可复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同的接口。也就是说,接口匹配使得我们可以将自己的类加入到一些现有的系统中去,而这些系统对这个类的接口可能会有所不同.

  3. 使用双向适配器提供透明操作
    使用适配器的一个潜在问题是,它们不对所有的客户都透明。被适配的对象不再兼容 Adaptee的接口,因此并不是所有 Adaptee对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性。在两个不同的客户需要用不同的方式查看同一个对象时,双向适配器尤其有用。

实现

让我们看看当API改变时,如何保护应用程序不受影响。
类适配器使用的是继承

#include <iostream>
using namespace std;

/**
 * 目标角色
 */
class Target
{
public:
    void hello(){}
    void world(){}
};

/**
 * 源角色:被适配的角色
 */
class Adaptee
{
public:
    void greet()
    {
        cout << "Greet" << endl;
    }

    void world()
    {
        cout << "World" << endl;
    }
};

/**
 * 类适配器角色
 */
class Adaptor: public Target, public Adaptee
{
public:
    void hello()
    {
        greet();
    }

    void world()
    {
        Adaptee::world();
    }
};

int main()
{
    Adaptor adaptor;
    adaptor.hello();
    adaptor.world();
    return 0;
} 

对象适配器使用的是委派

#include <iostream>
using namespace std;

/**
 * 目标角色
 */
class Target
{
public:
    void hello(){}
    void world(){}
};

/**
 * 源角色:被适配的角色
 */
class Adaptee
{
public:
    void greet()
    {
        cout << "Greet" << endl;
    }

    void world()
    {
        cout << "World" << endl;
    }
};

/**
 * 类适配器角色
 */
class Adaptor: public Target
{
public:
    explicit Adaptor(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }

    void hello()
    {
        _adaptee.greet();
    }

    void world()
    {
        _adaptee.world();
    }

private:
    Adaptee _adaptee;
};

int main()
{
    Adaptee adaptee;
    Adaptor adaptor(adaptee);
    adaptor.hello();
    adaptor.world();
    return 0;
}

如例中代码所示,你可以运用适配器(Adapter)模式来避免因外部库改变所带来的不便——倘若向上兼容。作为某个库的开发者,你应该编写适配器,使你的用户更简便地使用新版本的库,而不用他们去修改现有的全部代码。

GoF书中提出的适配器(Adapter)模式更倾向于运用继承而不是组成。这在强类型语言中是有利的,因为适配器(Adapter)事实上是一个目标类的子类,因而能更好地与类中方法相结合。

为了更好的灵活性,我个人比较倾向于组成的方法(特别是在结合了依赖性倒置的情况下);尽管如此,继承的方法提供两种版本的接口,或许在你的实际运用中反而是一个提高灵活性的关键。

适配器模式与其它相关模式

  • 桥梁模式(bridge):桥梁模式与对象适配器类似,但是桥梁模式的出发点不同:桥梁模式目的是将接口部分和实现部分分离,从而对它们可以较为容易也相对独立的加以改变。而对象适配器模式则意味着改变一个已有对象的接口

  • 装饰器模式(decorator):装饰模式增强了其他对象的功能而同时又不改变它的接口。因此装饰模式对应用的透明性比适配器更好。结果是decorator模式支持递归组合,而纯粹使用适配器是不可能实现这一点的。

  • 外观模式(Facade):适配器模式的重点是改变一个单独类的API。Facade的目的是给由许多对象构成的整个子系统,提供更为简洁的接口。而适配器模式就是封装一个单独类,适配器模式经常用在需要第三方API协同工作的场合,设法把你的代码与第三方库隔离开来。
    适配器模式与外观模式都是对现相存系统的封装。但这两种模式的意图完全不同,前者使现存系统与正在设计的系统协同工作而后者则为现存系统提供一个更为方便的访问接口。简单地说,适配器模式为事后设计,而外观模式则必须事前设计,因为系统依靠于外观。总之,适配器模式没有引入新的接口,而外观模式则定义了一个全新的接口。

  • 代理模式(Proxy)在不改变它的接口的条件下,为另一个对象定义了一个代理。

装饰者模式,适配器模式,外观模式三者之间的区别:

  • 装饰者模式的话,它并不会改变接口,而是将一个一个的接口进行装饰,也就是添加新的功能。
  • 适配器模式是将一个接口通过适配来间接转换为另一个接口。
  • 外观模式的话,其主要是提供一个整洁的一致的接口给客户端。

参考链接

组合模式

概述

在数据结构里面,树结构是很重要,我们可以把树的结构应用到设计模式里面。

例子1:就是多级树形菜单。
例子2:文件和文件夹目录

问题

如果要你实现一个管理文件和文件夹目录的类,你会如何设计?
一般都会想到分成两个类:文件类和文件目录类。文件目录类里面用一个数组保存保存着属于这个目录下的文件对象或者文件目录对象,还有几个用来管理这个数组的成员函数,比如add,remove,遍历等。文件类只需要保存文件信息就行。

这个想法是合理的,但是还是不够完美的。
文件和文件目录实质上是部分和整体关系,它们有着很多公共属性和操作。那如果把这些公共的属性和操作抽象成一个基类,文件类和文件目录类都继承自这个基类是不是更好。这样文件类和文件目录类就拥有统一对外接口,也方便了客户端的调用。

我们可以使用简单的对象组合成复杂的对象,而这个复杂对象有可以组合成更大的对象。我们可以把简单这些对象定义成类,然后定义一些容器类来存储这些简单对象。客户端代码必须区别对象简单对象和容器对象,而实际上大多数情况下用户认为它们是一样的。对这些类区别使用,使得程序更加复杂,递归使用的时候需要判断类型也很麻烦。我们如何使用递归组合,使得用户不必对这些类进行区别呢?

解决方案

组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。

组合模式让你可以优化处理递归分级数据结构

结构

典型的Composite对象结构如下图所示:
Composite

组合模式的分类

  1. 将管理子元素的方法定义在Composite类中
  2. 将管理子元素的方法定义在Component接口中,这样Leaf类就需要对这些方法空实现。

适用性

以下情况下适用Composite模式:

  1. 你想表示对象的部分-整体层次结构
  2. 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象

组合模式的组成

  • 抽象构件角色(component):是组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。这个接口可以用来管理所有的子对象。(可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
  • 树叶构件角色(Leaf):在组合树中表示叶节点对象,叶节点没有子节点。并在组合中定义图元对象的行为。
  • 树枝构件角色(Composite):定义有子部件的那些部件的行为。存储子部件。在Component接口中实现与子部件有关的操作。
  • 客户角色(Client):通过component接口操纵组合部件的对象。

效果

  1. 定义了包含基本对象和组合对象的类层次结构
    基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。客户代码中,任何用到基本对象的地方都可以使用组合对象。
  2. 简化客户代码
    客户可以一致地使用组合结构和单个对象。通常用户不知道 (也不关心)处理的是一个叶节点还是一个组合组件。这就简化了客户代码 , 因为在定义组合的那些类中不需要写一些充斥着选择语句的函数。
  3. 使得更容易增加新类型的组件
    新定义的Composite或Leaf子类自动地与已有的结构和客户代码一起工作,客户程序不需因新的Component类而改变。
  4. 使你的设计变得更加一般化
    很容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。有时你希望一个组合只能有某些特定的组件。使用Composite时,你不能依赖类型系统施加这些约束,而必须在运行时刻进行检查。

实现

比较经典的例子是树形菜单。多级展示,这个菜单可以无限增加节点;例外就是文件遍历等等。

/** 
 * 抽象构件角色(component) 
 * 
 */  
#include <iostream>
#include <map>
#include <string>

using namespace std;

class MenuComponent
{
public:
    virtual void add(MenuComponent &component) = 0;
    virtual void remove(MenuComponent &component) = 0;
    // virtual string getUrl() = 0;
    virtual void displayOperation(string align) = 0;
    string getName()
    {
        return _name;
    }


protected:
    string _name;
};

/**
 * 树枝构件角色(Composite)
 *
 */
class MenuComposite: public MenuComponent
{
public:
    explicit MenuComposite(string name)
    {
        _name = name;
    }

    void add(MenuComponent &component)
    {
        _items[component.getName()] = &component;
    }

    void remove(MenuComponent &component)
    {
        map<string, MenuComponent *>::iterator it;
        for (it = _items.begin(); it != _items.end(); it++)
        {
            if (it->first == component.getName())
            {
                _items.erase(it);
            }
        }
    }

    void displayOperation(string align)
    {
        if (!_items.empty())
        {
            align += "--";
        }
        cout << _name << endl;
        map<string, MenuComponent *>::iterator it;
        for (it = _items.begin(); it != _items.end(); it++)
        {
            cout << align;
            it->second->displayOperation(align);
        }
    }

    map<string, MenuComponent *> getItems()
    {
        return _items;
    }
private:
    map<string, MenuComponent *> _items;
};

/** 
 *树叶构件角色(Leaf) 
 * 
 */
class Leaf: public MenuComponent
{
public:
    Leaf(string name, string url)
    {
        _name = name;
        _url = url;
    }

    void add(MenuComponent &component){}
    void remove(MenuComponent &component){}

    void displayOperation(string align)
    {
        cout << _name << "_" << _url << endl;
    }

    void getItems()
    {

    }

private:
    string _url;
};

int main()
{
    MenuComposite submenu1("submenu1");
    MenuComposite submenu2("submenu2");
    MenuComposite submenu3("submenu3");
    MenuComposite submenu4("submenu4");
    MenuComposite submenu5("submenu5");

    Leaf item1("baidu", "www.baidu.com");
    Leaf item2("google", "www.google.com");
    submenu2.add(item1);
    submenu2.add(item2);

    submenu3.add(submenu2);
    submenu3.add(submenu4);
    submenu3.add(submenu5);

    MenuComposite allmenu("allmenu");
    allmenu.add(submenu1);
    allmenu.add(submenu2);
    allmenu.add(submenu3);

    allmenu.displayOperation("|");
    return 0;
}

输出

allmenu
|--submenu1
|--submenu2
|----baidu_www.baidu.com
|----google_www.google.com
|--submenu3
|----submenu2
|------baidu_www.baidu.com
|------google_www.google.com
|----submenu4
|----submenu5

组合模式和其他相关模式

  1. 装饰模式(Decorator模式)经常与Composite模式一起使用。当装饰和组合一起使用时,它们通常有一个公共的父类。因此装饰必须支持具有 Add、Remove和GetChild 操作的Component接口。
  2. Flyweight模式让你共享组件,但不再能引用他们的父部件。
  3. (迭代器模式)Itertor可用来遍历Composite。
  4. (观察者模式)Visitor将本来应该分布在Composite和Leaf类中的操作和行为局部化。

总结

组合模式解耦了客户程序与复杂元素内部结构,从而使客户程序可以向处理简单元素一样来处理复杂元素。

如果你想要创建层次结构,并可以在其中以相同的方式对待所有元素,那么组合模式就是最理想的选择。

参考链接