文章目录
- 四、结构型模式
- 4.4 DECORATOR(装饰)——对象结构型模式
- 1.意图
- 2.别名
- 补充部分
- 3.动机
- 4.适用性
- 5.结构
- 6.参与者
- 7.协作
- 8.效果
- 9.实现
- 10.代码示例
- 11.相关模式
- 4.5 FACADE(外观)
- 1.意图
- 2.动机
- 3.适用性
- 4.结构
- 5.参与者
- 6.协作
- 7.效果
- 8.实现
- 9.代码示制
- 10.相关模式
- 4.6 FLYWEIGHT(享元)
- 1.意图
- 2.动机
- 3.适用性
- 4.结构
- 5.参与者
- 6.协作
- 7.效果
- 8.实现
- 9.代码示例
- 10.相关模式
- 4.7 PROXY(代理)
- 1.意图
- 2.别名
- 3.动机
- 4.适用性
- 5.结构
- 6.参与者
- 7.协作
- 8.效果
- 9.实现
- 10.代码示例
- 11.相关模式
- 资料
四、结构型模式
4.4 DECORATOR(装饰)——对象结构型模式
1.意图
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
2.别名
包装器Wrapper
补充部分
// 业务操作
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 Stream{
public:virtual char Read(int number){// 读取文件}virtual void Seek(int position){// 定位文件流}virtual void Write(char data){// 写文件流}
};class NetworkStream:public Stream{
public:virtual char Read(int number){// 读网络流}virtual void Seek(int position){// 定位网络流}virtual void Write(char data){// 写网络流}
};// 扩展操作
class CryptoFileStream:public FileStream{
public:virtual char Read(int number){// 额外的加密操作...FileStream::Read(number);}virtual char Seek(int position){// 额外的加密操作...FileStream::Seek(position);}virtual void Write(char data){// 额外的加密操作...FileStream::Write(data);}
};class CryptoNetworkStream:public NetworkStream{
NetworkStream* stream;
public:virtual char Read(int number){// 额外的加密操作...NetworkStream::Read(number);}virtual char Seek(int position){// 额外的加密操作...NetworkStream::Seek(position);}virtual void Write(char data){// 额外的加密操作...NetworkStream::Write(data);}
};
这是我们发现,我们额外操作其实是一样的,但多了很多代码(冗余)。如果,我们还要进行缓冲操作,又会叠一层;此外,我们如果既要加密又要缓冲,这又是一系列的实现。这将对我们的实现带来巨大的挑战。
于是我们进行如下修改
// 扩展操作
// 此时我们继承Stream是为了接口一致
class CryptoStream:public Stream{
private:Stream* stream;
public:CryptoStream(Stream* stream):stream(stream){}virtual char Read(int number){// 额外的加密操作...stream->Read(number);}virtual char Seek(int position){// 额外的加密操作...stream->Seek(position);}virtual void Write(char data){// 额外的加密操作...stream->Write(data);}
};class BufferStream:public Stream{
private:Stream* stream;
public:BufferStream(Stream* stream):stream(stream){}virtual char Read(int number){// 额外的缓存操作...stream->Read(number);}virtual char Seek(int position){// 额外的缓存操作...stream->Seek(position);}virtual void Write(char data){// 额外的缓存操作...stream->Write(data);}
};
此时我们发现,我们Stream* stream也冗余了于是我们使用修饰类
// 修饰类
class DecoratorStream:public Stream{
protected:Stream* stream;//...
};class CryptoStream:public DecoratorStream{
public:CryptoStream(Stream* stream):stream(stream){}virtual char Read(int number){// 额外的加密操作...stream->Read(number);}virtual char Seek(int position){// 额外的加密操作...stream->Seek(position);}virtual void Write(char data){// 额外的加密操作...stream->Write(data);}
};class BufferStream:public DecoratorStream{
public:BufferStream(Stream* stream):stream(stream){}virtual char Read(int number){// 额外的缓存操作...stream->Read(number);}virtual char Seek(int position){// 额外的缓存操作...stream->Seek(position);}virtual void Write(char data){// 额外的缓存操作...stream->Write(data);}
};
3.动机
有时我们希望给某个对象而不是整个类添加一些功能。例如,一个图形用户界面工具箱允许你对任意一个用户界面组件添加一些特性,例如边框,或是一些行为,例如窗口滚动。
使用继承机制是添加功能的一种有效途径,从其他类继承过来的边框特性可以被多个子类的实例所使用。但这种方法不够灵活,因为边框的选择是静态的,用户不能控制对组件加边框的方式和时机。
一种较为灵活的方式是将组件嵌入另一个对象中,由这个对象添加边框。我们称这个嵌入的对象为装饰。这个装饰与它所装饰的组件接口-致,因此它对使用该组件的客户透明。
它将客户请求转发给该组件,并且可能在转发前后执行一些额外的动作(例如画一个边框)。透明性使得你可以递归的嵌套多个装饰,从而可以添加任意多的功能,如下图所示。
例如,假定有一个对象TextView,它可以在窗口中显示正文。缺省的TextView没有滚动条,因为我们可能有时并不需要滚动条。当需要滚动条时,我们可以用ScrollDecorator添加滚动条。如果我们还想在TextView周围添加一个粗黑边框,可以使用BorderDecorator添加。因此只要简单地将这些装饰和TextView进行组合,就可以达到预期的效果。
下面的对象图展示了如何将一个TextView对象与BorderDecorator以及ScrollDecorator对象组装起来产生一个具有边框和滚动条的文本显示窗口。
ScrollDecorator和BorderDecorator类是Decorator类的子类。Decorator类是一个可视组件的抽象类,用于装饰其他可视组件,如下图所示。
VisualComponent是一个描述可视对象的抽象类,它定义了绘制和事件处理的接口。注意Decorator类怎样将绘制请求简单地发送给它的组件,以及Decorator的子类如何扩展这个操作。
Decorator的子类为特定功能可以自由地添加一些操作。例如,如果其他对象知道界面中恰好有一个ScrollDecorator对象,这些对象就可以用ScrollDecorator对象的ScrollTo操作滚动这个界面。这个模式中有一点很重要,它使得在VisualComponent可以出现的任何地方都可以有装饰。因此,客户通常不会感觉到装饰过的组件与未装饰组件之间的差异,也不会与装饰产生任何依赖关系。
4.适用性
以下情况使用Decorator模式
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 处理那些可以撤消的职责。
- 当不能采用生成子类的方法进行扩充时。
- 一种情况是,可能有大量独立的扩展,为支持每种组合将产生大量的子类,使得子类数目呈爆炸性增长。
- 另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
5.结构
6.参与者
Component ( visualComponent)
- 定义一个对象接口,可以给这些对象动态地添加职责。
ConcreteComponent (TextView)
- 定义一个对象,可以给这个对象添加一些职责。
Decorator
- 维持一个指向Component对象的指针,并定义一个与Component接口一致的接口。
ConcreteDecorator (BorderDecorator,ScrollDecorator)
- 向组件添加职责。
7.协作
Decorator将请求转发给它的Component对象,并有可能在转发请求前后执行一些附加的动作。
8.效果
Decorator模式至少有两个主要优点和两个缺点:
- 比静态继承更灵活
与对象的静态继承(多重继承)相比,Decorator模式提供了更加灵活的向对象添加职责的方式。可以用添加和分离的方法,用装饰在运行时刻增加和删除职责。相比之下,继承机制要求为每个添加的职责创建一个新的子类(例如,BorderScrollableTextView, BorderedTextView )。这会产生许多新的类,并且会增加系统的复杂度。此外,为一个特定的Component类提供多个不同的 Decorator类,这就使得你可以对一些职责进行混合和匹配。
使用Decorator模式可以很容易地重复添加一个特性,例如在TextView上添加双边框时,仅需将添加两个BorderDecorator即可。而两次继承Border类则极容易出错的。
- 避免在层次结构高层的类有太多的特征
Decorator模式提供了一种“即用即付”的方法来添加职责。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可以定义一个简单的类,并且用Decorator类给它逐渐地添加功能。可以从简单的部件组合出复杂的功能。这样,应用程序不必为不需要的特征付出代价。同时也更易于不依赖于Decorator所扩展(甚至是不可预知的扩展)的类而独立地定义新类型的Decorator。扩展一个复杂类的时候,很可能会暴露与添加的职责无关的细节。
- Decorator与它的Component不一样
Decorator是一个透明的包装。如果我们从对象标识的观点出发,一个被装饰了的组件与这个组件是有差别的,因此,使用装饰时不应该依赖对象标识。
- 有许多小对象
采用Decorator模式进行系统设计往往会产生许多看上去类似的小对象,这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。
9.实现
使用Decorator模式时应注意以下几点:
- 接口的一致性
装饰对象的接口必须与它所装饰的Component的接口是一致的,因此,所有的ConcreteDecorator类必须有一个公共的父类(至少在C++中如此)。
- 省略抽象的Decorator类
当你仅需要添加一个职责时,没有必要定义抽象Decorator类。你常常需要处理现存的类层次结构而不是设计一个新系统,这时你可以把Decorator向Component转发请求的职责合并到ConcreteDecorator中。
- 保持Component类的简单性
为了保证接口的一致性,组件和装饰必须有一个公共的Component父类。因此保持这个类的简单性是很重要的;即,它应集中于定义接口而不是存储数据。对数据表示的定义应延迟到子类中,否则Component类会变得过于复杂和庞大,因而难以大量使用。赋予Component太多的功能也使得,具体的子类有一些它们并不需要的功能的可能性大大增加。
- 改变对象外壳与改变对象内核
我们可以将Decorator看作一个对象的外壳,它可以改变这个对象的行为。另外一种方法是改变对象的内核。例如,Strategy(5.9)模式就是一个用于改变内核的很好的模式。
当Component类原本就很庞大时,使用Decorator模式代价太高,Strategy模式相对更好一些。在Strategy模式中,组件将它的一些行为转发给一个独立的策略对象,我们可以替换strategy对象,从而改变或扩充组件的功能。
例如我们可以将组件绘制边界的功能延迟到一个独立的Border对象中,这样就可以支持不同的边界风格。这个Border对象是一个Strategy对象,它封装了边界绘制策略。我们可以将策略的数目从一个扩充为任意多个,这样产生的效果与对装饰进行递归嵌套是一样的。
在MacApp3.0和Bedrock中,绘图组件(称之为“视图”)有一个“装饰”(adorner)对象列表,这些对象可用来给一个视图组件添加一些装饰,例如边框。如果给一个视图添加了一些装饰,就可以用这些装饰对这个视图进行一些额外的修饰。由于View类过于庞大,MacApp和Bedrock必须使用这种方法。仅为添加一个边框就使用一个完整的View,代价太高。
由于Decorator模式仅从外部改变组件,因此组件无需对它的装饰有任何了解;也就是说,这些装饰对该组件是透明的,如下图所示。
在Strategy模式中,component组件本身知道可能进行哪些扩充,因此它必须引用并维护相应的策略,如下图所示。
基于Strategy的方法可能需要修改component组件以适应新的扩充。另一方面,一个策略可以有自己特定的接口,而装饰的接口则必须与组件的接口一致。
例如,一个绘制边框的策略仅需要定义生成边框的接口( DrawBorder,GetWidth等),这意味着即使Component类很庞大时,策略也可以很小。
MacApp和Bedrock中,这种方法不仅仅用于装饰视图,还用于增强对象的事件处理能力。在这两个系统中,每个视图维护一个“行为”对象列表,这些对象可以修改和截获事件。
在已注册的行为对象被没有注册的行为有效的重定义之前,这个视图给每个已注册的对象一个处理事件的机会。可以用特殊的键盘处理支持装饰一个视图,例如,可以注册一个行为对象截取并处理键盘事件。
10.代码示例
以下C++代码说明了如何实现用户接口装饰。我们假定已经存在一个Component类VisualComponent。
class VisualComponent{
public:Visualcomponent();virtual void Draw ();virtual void Resize(); // ...
) ;
我们定义VisualComponent的一个子类Decorator,我们将生成Decorator的子类以获取不同的装饰。
ciass Decorator : public visualcomnponent{
public:Decorator(visualComponent*);virtual void Draw();virtual void Resize(); // ...
private:Visualcomponent* _component;
};
Decorator装饰由_.component实例变量引用的VisualComponent,这个实例变量在构造器中被初始化。
对于VisualComponent接口中定义的每一个操作,Decorator类都定义了一个缺省的实现,这一实现将请求转发给_component:
voia Decorator::Draw(){_component->Draw();
}
void Decorator::Resize(){_component->Resize();
}
Decorator的子类定义了特殊的装饰功能,例如,BorderDecorator类为它所包含的组件添加了一个边框。BorderDecorator是Decorator的子类,它重定义Draw操作用于绘制边框。
同时BorderDecorator还定义了一个私有的辅助操作DrawBorder,由它绘制边框。这些子类继承了Decorator类所有其他的操作。
class BorderDecorator:public Decorator{
public:BorderDecorator(Visualcomponent*, int borderwidth);virtual void Draw();
private:void DrawBorder (int);int _width;
};
void BorderDecorator::Draw(){Decorator::Draw();DrawBorder(_width);
}
类似的可以实现ScrollDecorator和DropShadowDecorator,它们给可视组件添加滚动和阴影功能。
现在我们组合这些类的实例以提供不同的装饰效果,以下代码展示了如何使用Decorator创建一个具有边界的可滚动TextView。
首先我们要将一个可视组件放入窗口对象中。我们假设Window类为此已经提供了一个SetContents操作:
void Window::setContents (VisualComponent* contents) {// ...
}
现在我们可以创建一个正文视图以及放人这个正文视图的窗口:
window* window = new window ;
TextviewrtextView = new Textview;
Text View是一个VisualComponent,它可以放人窗口中:
window->SetContents(textview);
但我们想要一个有边界的和可以滚动的TextView,因此我们在将它放入窗口之前对其进行装饰:
windowa->Setcontents(new BorderDecorator(new scrollDecorator(textvierwe),1)
);
由于Window通过VisualComponent接口访问它的内容,因此它并不知道存在该装饰。如果你需要直接与正文视图交互,例如,你想调用一些操作,而这些操作不是VisualComponent接口的一部分,此时你可以跟踪正文视图。依赖于组件标识的客户也应该直接引用它。
11.相关模式
Adapter(4.1)模式:Decorator模式不同于Adapter模式,因为装饰仅改变对象的职责而不改变它的接口;而适配器将给对象一个全新的接口。
Composite(4.3)模式:可以将装饰视为一个退化的、仅有一个组件的组合。然而,装饰仅给对象添加一些额外的职责——它的目的不在于对象聚集。
Strategy(5.9)模式:用一个装饰你可以改变对象的外表;而Strategy模式使得你可以改变对象的内核。这是改变对象的两种途径。
4.5 FACADE(外观)
1.意图
为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
2.动机
将一个系统划分成为若干个子系统有利于降低系统的复杂性。
一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小。达到该目标的途径之一是就是引入一个外观( facade)对象,它为子系统中较一般的设施提供了一个单一而简单的界面。
例如有一个编程环境,它允许应用程序访问它的编译子系统。这个编译子系统包含了若干个类,如Scanner、Parser、ProgramNode、BytecodeStream和ProgramNodeBuilder,用于实现这一编译器。有些特殊应用程序需要直接访问这些类,但是大多数编译器的用户并不关心语法分析和代码生成这样的细节;他们只是希望编译一些代码。对这些用户,编译子系统中那些功能强大但层次较低的接口只会使他们的任务复杂化。
为了提供一个高层的接口并且对客户屏蔽这些类,编译子系统还包括一个Complier类。这个类定义了一个编译器功能的统一接口。Compiler类是一个外观,它给用户提供了一个单一而简单的编译子系统接口。它无需完全隐藏实现编译功能的那些类,即可将它们结合在一起。编译器的外观可方便大多数程序员使用,同时对少数懂得如何使用底层功能的人,它并不隐藏这些功能,如下图所示。
3.适用性
在遇到以下情况使用Facade模式
- 当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。Facade可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过facade层。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
- 当你需要构建一个层次结构的子系统时,使用facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过facade进行通讯,从而简化了它们之间的依赖关系。
4.结构
5.参与者
- Facade (Compiler)
- 知道哪些子系统类负责处理请求。
- 将客户的请求代理给适当的子系统对象。
- Subsystem classes (Scanner、Parser、ProgramNode等)
- 实现子系统的功能。
- 处理由Facade对象指派的任务。
- 没有facade的任何相关信息;即没有指向facade的指针。
6.协作
- 客户程序通过发送请求给Facade的方式与子系统通讯,Facade将这些消息转发给适当的子系统对象。尽管是子系统中的有关对象在做实际工作,但Facade模式本身也必须将它的接口转换成子系统的接口。
- 使用Facade的客户程序不需要直接访问子系统对象。
7.效果
Facade模式有下面一些优点:
- 它对客户屏蔽子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。
- 它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。松耦合关系使得子系统的组件变化不会影响到它的客户。Facade模式有助于建立层次结构系统,也有助于对对象之间的依赖关系分层。Facade模式可以消除复杂的循环依赖关系。这一点在客户程序与子系统是分别实现的时候尤为重要。
在大型软件系统中降低编译依赖性至关重要。在子系统类改变时,希望尽量减少重编译工作以节省时间。用Facade可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。Facade模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。
- 如果应用需要,它并不限制它们使用子系统类。因此你可以在系统易用性和通用性之间加以选择。
8.实现
使用Facade模式时需要注意以下几点:
- 降低客户-子系统之间的耦合度
瞭用抽象类实现Facade而它的具体子类对应于不同的子系统实现,这可以进一步降低客户与子系统的费合度。这样,客户就可以通过抽象的Facade类接口与子系统通讯。这种抽象耦合关系使得客户不知道它使用的是子系统的哪一个实现。
除生成子类的方法以外,另一种方法是用不同的子系统对象配置Facade对象。为定制facade,仅需对它的子系统对象(一个或多个)进行替换即可。
- 公共子系统类与私有子系统类
一个子系统与一个类的相似之处是,它们都有接口并且它们都封装了一些东西——类封装了状态和操作,而子系统封装了一些类。考虑一个类的公共和私有接口是有益的,我们也可以考虑子系统的公共和私有接口。
子系统的公共接口包含所有的客户程序可以访问的类;私有接口仅用于对子系统进行扩充。当然,Facade类是公共接口的一部分,但它不是唯一的部分,子系统的其他部分通常也是公共的。例如,编译子系统中的Parser类和Scanner类就是公共接口的一部分。
私有化子系统类确实有用,但是很少有面向对象的编程语言支持这一点。C++和Smalltalk语言仅在传统意义下为类提供了一个全局名空间。然而,最近C++标准化委员会在C++语言中增加了一些名字空间,这些名字空间使得你可以仅暴露公共子系统类。
9.代码示制
让我们仔细观察一下如何在一个编译子系统中使用Facade。
编译子系统定义了一个BytecodeStream类,它实现了一个Bytecode对象流( stream )。Bytecode对象封装一个字节码,这个字节码可用于指定机器指令。该子系统中还定义了一个Token类,它封装了编程语言中的标识符。
Scanner类接收字符流并产生一个标识符流,一次产生一个标识符(token)。
clase Scanner{
public:Scanner(istreams&);virtual ~Scanner();virtual Token& Scan();
private:istream& _inputstream;
};
用ProgramNodeBuilder,Parser类由Scanner生成的标识符构建一棵语法分析树。
class Parser{
public:Parser();virtuai ~Parser(:virtual void Parse (Scanner&, ProgramNodeBuilder&);
};
Parser回调ProgramNodeBuilder逐步建立语法分析树,这些类遵循Builder(3.2)模式进行交互操作。
class ProgramkodeBuilder{
public:ProgramNodeBuilder()virtual ProgramNode* NewVariable(const char* variableName) const;virtual ProgramNode* NewAssignment (ProgramNode* variable,ProgramNode* expression) const;virtual ProgramNode* NewReturnstatement(ProgramNode* value) const;virtual ProgramNode* Newcondition (ProgramiNode condition,ProgramNode* truePart,,ProgramNode*falsePart)const;//...Programode* GetRootNode();
private:Programode* _node:
};
语法分析树由ProgramNode子类(例如StatementNode和ExpressionNode等)的实例构成。ProgramNode层次结构是Composite模式的一个应用实例。ProgramNode定义了一个接口用于操作程序节点和它的子节点(如果有的话)。
ciass ProgramnNode{
public:// program node manipulationvirtual void GetSourcePosition (int& line,int& index);// ...// child manipulationvirtual voia Add (ProgramNode*);virtual void Remove (ProgramNode*);// ...virtual void Traverse (codeGenerator&) ;
protected:ProgramNode();
};
Traverse操作以一个CodeGenerator对象为参数,ProgramNode子类使用这个对象产生机器代码,机器代码格式为BytecodeStream中的ByteCode对象。其中的CodeGenerator类是一个访问者(参见Visitor(5.11)模式)。
class CodeGenerator{
public:virtual void Visit(StatemnentNode*);virtuai void Visit(ExpressionNode*);// ...
protected:CodeGenerator(BytecodeStream&);
protected:BytecodeStream& _output;
};
例如CodeGenerator类有两个子类StackMachineCodeGenerator和RISCCodeGenerator,分别为不同的硬件体系结构生成机器代码。
ProgramNode的每个子类在实现Traverse时,对它的ProgramNode子对象调用Traverse。每个子类依次对它的子节点做同样的动作,这样一直递归下去。
例如,ExpressionNode像这样定义Traverse:
void ExpressionNode::Traverse(CodeGenerator& cg) {cg.visit (this):ListIterator<Prograrbode*> i(_children);for(i.First();!i.IsDoned();i.Next()){i.CurrentItem()->Traverse(cg);}
}
我们上述讨论的类构成了编译子系统,现在我们引入Compiler类,Complier类是一个facade,它将所有部件集成在一起。Compiler提供了一个简单的接口用于为特定的机器编译源代码并生成可执行代码。
class Compliler{
public:Compiler();virtual void Compile(istream&,BytecodeStream&);
};void Compiler::Compile(istream& input,Bytecodestream& output){Scanner scanner(input):ProgramNodeBuilder builder;Parser parser;parser.Parse(scanner,builder);RISCCodeGenerator generator(output);ProgramNode* parseTree = builder.GetRootNode();parseTree->Traverse(generator);
}
上面的实现在代码中固定了要使用的代码生成器的种类,因此程序员不需要指定目标机的结构。在仅有一种目标机的情况下,这是合理的。
如果有多种目标机,我们可能希望改变Compiler构造函数使之能接受CodeGenerator为参数,这样程序员可以在实例化Compiler时指定要使用的生成器。
编译器的facade还可以对Scanner和ProgramNodeBuilder这样的其他一些参与者进行参数化以增加系统的灵活性,但是这并非Facade模式的主要任务,它的主要任务是为一般情况简化接口。
10.相关模式
Abstract Factory (3.1)模式可以与Facade模式一起使用以提供一个接口,这一接口可用来以一种子系统独立的方式创建子系统对象。Abstract Factory也可以代替Facade模式隐藏那些与平台相关的类。
Mediator (5.5)模式与Facade模式的相似之处是,它抽象了一些已有的类的功能。然而,Mediator的目的是对同事之间的任意通讯进行抽象,通常集中不属于任何单个对象的功能。Mediator的同事对象知道中介者并与它通信,而不是直接与其他同类对象通信。
相对而言,Facade模式仅对子系统对象的接口进行抽象,从而使它们更容易使用;它并不定义新功能,子系统也不知道facade的存在。
通常来讲,仅需要一个Facade对象,因此Facade对象通常属于Singleton (3.5)模式。
4.6 FLYWEIGHT(享元)
1.意图
运用共享技术有效地支持大量细粒度的对象。
2.动机
有些应用程序得益于在其整个设计过程中采用对象技术,但简单化的实现代价极大。
例如,大多数文档编辑器的实现都有文本格式化和编辑功能,这些功能在一定程度上是模块化的。面向对象的文档编辑器通常使用对象来表示嵌入的成分,例如表格和图形。尽管用对象来表示文档中的每个字符会极大地提高应用程序的灵活性,但是这些编辑器通常并不这样做。字符和嵌入成分可以在绘制和格式化时统–处理,从而在不影响其他功能的情况下能对应用程序进行扩展,支持新的字符集。应用程序的对象结构可以模拟文档的物理结构。下图显示了一个文档编辑器怎样使用对象来表示字符。
但这种设计的缺点在于代价太大。即使是一个中等大小的文档也可能要求成百上千的字符对象,这会耗费大量内存,产生难以接受的运行开销。所以通常并不是对每个字符都用一个对象来表示的。Flyweight模式描述了如何共享对象,使得可以细粒度地使用它们而无需高昂的代价。
flyweight是一个共享对象,它可以同时在多个场景(context)中使用,并且在每个场景中flyweight都可以作为一个独立的对象——这一点与非共享对象的实例没有区别。flyweight不能对它所运行的场景做出任何假设,这里的关键概念是内部状态和外部状态之间的区别。内部状态存储于flyweight中,它包含了独立于flyweight场景的信息,这些信息使得flyweight可以被共享。而外部状态取决于Flyweight场景,并根据场景而变化,因此不可共享。用户对象负责在必要的时候将外部状态传递给Flyweight。
Flyweight模式对那些通常因为数量太大而难以用对象来表示的概念或实体进行建模。例如,文档编辑器可以为字母表中的每一个字母创建一个flyweight。每个flyweight存储一个字符代码,但它在文档中的位置和排版风格可以在字符出现时由正文排版算法和使用的格式化命令决定。字符代码是内部状态,而其他的信息则是外部状态。
逻辑上,文档中的给定字符每次出现都有一个对象与其对应,如下图所示。
然而,物理上每个字符共享一个flyweight对象,而这个对象出现在文档结构中的不同地方。一个特定字符对象的每次出现都指向同一个实例,这个实例位于flyweight对象的共享池中。
这些对象的类结构如下图所示。Glyph是图形对象的抽象类,其中有些对象可能是flyweight。基于外部状态的那些操作将外部状态作为参量传递给它们。
例如,Draw和Intersects在执行之前,必须知道glyph所在的场景,如下图所示。
表示字母“a”的flyweight只存储相应的字符代码;它不需要存储字符的位置或字体。用户提供与场景相关的信息,根据此信息flyweight绘出它自己。例如,Row glyph知道它的子女应该在哪儿绘制自己才能保证它们是横向排列的。因此Row glyph可以在绘制请求中向每一个子女传递它的位置。
由于不同的字符对象数远小于文档中的字符数,因此,对象的总数远小于一个初次执行的程序所使用的对象数目。对于一个所有字符都使用同样的字体和颜色的文档而言,不管这个文档有多长,需要分配100个左右的字符对象(大约是ASCII字符集的数目)。由于大多数文档使用的字体颜色组合不超过10种,实际应用中这一数目不会明显增加。因此,对单个字符进行对象抽象是具有实际意义的。
3.适用性
Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都成立时使用Flyweight模式:
- 一个应用程序使用了大量的对象。
- 完全由于使用大量的对象,造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
- 应用程序不依赖于对象标识。由于Flyweight对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。
4.结构
如何共享flyweights
5.参与者
- Flyweight (Glyph)
- 描述一个接口,通过这个接口flyweight可以接受并作用于外部状态。
- ConcreteFlyweight(Character)
- 实现Flyweight接口,并为内部状态(如果有的话)增加存储空间。
ConcreteFlyweight对象必须是可共享的。它所存储的状态必须是内部的;即,它必须独立于ConcreteFlyweight对象的场景。
- 实现Flyweight接口,并为内部状态(如果有的话)增加存储空间。
- UnsharedConcreteFlyweight (Row,Column)
- 并非所有的Flyweight子类都需要被共享。Flyweight接口使共享成为可能,但它并不强制共享。在Flyweight对象结构的某些层次,UnsharedConcreteFlyweight对象通常将ConcreteFlyweight对象作为子节点(Row和Column就是这样)。
- FlyweightFactory
- 创建并管理flyweight对象。
- 确保合理地共享flyweight。当用户请求一个flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话.)。
- Client
- 维持一个对flyweight的引用。
- 计算或存储–个(多个) flyweight的外部状态。
6.协作
-
flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于ConcreteFlyweight对象之中;而外部对象则由Client对象存储或计算。当用户调用flyweight对象的操作时,将该状态传递给它。
-
用户不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享。
7.效果
使用Flyweight模式时,传输、查找和或计算外部状态都会产生运行时的开销,尤其当flyweight原先被存储为内部状态时。然而,空间上的节省抵消了这些开销。共享的flyweight越多,空间节省也就越大。
存储节约由以下几个因素决定:
- 因为共享,实例总数减少的数目
- 对象内部状态的平均数目
- 外部状态是计算的还是存储的
共享的Flyweight越多,存储节约也就越多。节约量随着共享状态的增多而增大。当对象使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最大。所以,可以用两种方法来节约存储:用共享减少内部状态的消耗,用计算时间换取对外部状态的存储。
Flyweight模式经常和Composite(4.3)模式结合起来表示一个层次式结构,这一层次式结构是一个共享叶节点的图。共享的结果是,Flyweight的叶节点不能存储指向父节点的指针。而父节点的指针将传给Flyweight作为它的外部状态的一部分。这对于该层次结构中对象之间相互通讯的方式将产生很大的影响。
8.实现
在实现Flyweight模式时,注意以下几点:
- 删除外部状态
该模式的可用性在很大程度上取决于是否容易识别外部状态并将它从共享对象中删除。如果不同种类的外部状态和共享前对象的数目相同的话,删除外部状态不会降低存储消耗。理想的状况是,外部状态可以由一个单独的对象结构计算得到,且该结构的存储要求非常小。
例如,在我们的文档编辑器中,我们可以用一个单独的结构存储排版布局信息,而不是存储每一个字符对象的字体和类型信息,布局图保持了带有相同排版信息的字符的运行轨迹。当某字符绘制自己的时候,作为绘图遍历的副作用它接收排版信息。因为通常文档使用的字体和类型数量有限,将该信息作为外部信息来存储,要比内部存储高效得多。
- 管理共享对象
因为对象是共享的,用户不能直接对它进行实例化,因此Flyweight-Factory可以帮助用户查找某个特定的Flyweight对象。FlyweightFactory对象经常使用关联存储帮助用户查找感兴趣的Flyweight对象。例如,在这个文档编辑器一例中的Flyweight工厂就有一个以字符代码为索引的Flyweight表。管理程序根据所给的代码返回相应的Flyweight,若不存在,则创建一个Flyweight。
共享还意味着某种形式的引用计数和垃圾回收,这样当一个Flyweight不再使用时,可以回收它的存储空间。然而,当Flyweight的数目固定而且很小的时候(例如,用于ACSII码的Flyweight ),这两种操作都不必要。在这种情况下,Flyweight完全可以永久保存。
9.代码示例
回到我们文档编辑器的例子,我们可以为Flyweight的图形对象定义一个Glyph基类。逻辑上,Glyph是一些Composite类(见Composite ( 4.3 )),它有图形化属性,并可以绘制自己。这里,我们重点讨论字体属性,但这种方法也同样适用于Glyph的其他图形属性。
class Glyph{
public:virtual ~Glyph ( ) ;virtual void Draw(window* , Glyphcontext&);virtual void setFont (Font* , GlyphContext&);virtual Font* GetFont (Glyphcontext&);virtual void First (GlyphContext&) ;virtual void Next (Glyphcontext& );virtual bool IsDone(Glyphcontext&);virtual GLyph*Current (Glyphcontext&);virtual void Insert (Glyph* , GlyphContext&);virtual void Remove(GlyphContext&);
protected:Glyph();
};
Character的子类存储一个字符代码:
class Character: public Glyph{
public: Character(char) ;virtual void Draw (Window* , GlyphContext&);
private:char _charcode;
};
为了避免给每一个Glyph的字体属性都分配存储空间,我们可以将该属性外部存储于GlyphContext对象中。GlyphContext是一个外部状态的存储库,它维持Glyph与字体(以及其他一些可能的图形属性)之间的一种简单映射关系。
对于任何操作,如果它需要知道在给定场景下Glyph字体,都会有一个GlyphContext实例作为参数传递给它。然后,该操作就可以查询GlyphContext以获取该场景中的字体信息了。这个场景取决于Glyph结构中的Glyph的位置。因此,当使用Glyph时,Glyph子类的迭代和管理操作必须更新GlyphContext
class clyphcontext{
public:
Glyphcontext ( ;virtual ~Glyphcontext (;virtual void Next (int step = 1);virtual voia Insert (int quantity = 1);virtual Font* GetFont();virtual void setFont (Font* , int span = 1);
private:int _index;BTree* _fonts;
};
在遍历过程中,GlyphContext必须它在Glyph结构中的当前位置。随着遍历的进行,GlyphContext::Next增加_index的值。Glyph的子类(如,Row和Column)对Next操作的实现必须使得它在遍历的每一点都调用GlyphContext::Next。
GlyphContext::GetFocus将索引作为Btree结构的关键字,Btree结构存储glyph到字体的映射。树中的每个节点都标有字符串的长度,而它给这个字符串字体信息。树中的叶节点指向一种字体,而内部的字符串分成了很多子字符串,每一个对应一种子节点。
下页上图是从一个glyph组合中截取出来的:
字体信息的BTree结构可能如下:
内部节点定义Glyph索引的范围。当字体改变或者在Glyph结构中添加或删除Glyph时,Btree将相应地被更新。例如,假定我们遍历到索引102,以下代码将单词“except”的每个字符的字体设置为它周围的正文的字体(即,用Time 12字体,12-point Times Roman的一个实例):
GlyphContext gc;
Font* times12 = new Font ( "Times-Romani-12");
Font* timesItalic12 = new Font ( "Times-italic-12");
// ...
gc.setFont (times12, 6);
新结构如下
假设我们要在单词“expect”前用12-point Times Italic字体添加一个单词Don’t(包括一个紧跟着的空格)。假定gc仍在索引位置102,以下代码通知gc这个事件:
gc.Insert(6);
gc.setFont (timesitalic12,6);
Btree结构变为如下图所示:
当向GlyphContext查询当前Glyph的字体时,它将向下搜寻Btree,同时增加索引,直至找到当前索引的字体为止。由于字体变化频率相对较低,所以这棵树相对于Glyph结构较小。这将使得存储耗费较小,同时也不会过多的增加查询时间。
FlyweightFactory是我们需要的最后一个对象,它负责创建Glyph并确保对它们进行合理共享。GlyphFactory类将实例化Character和其他类型的Glyph。我们只共享Character对象;组合的Glyph要少得多,并且它们的重要状态(如,他们的子节点)必定是内部的。\
const int NCHARCODES = 128;
class GlyphFactory f
public:GlyphFactory();virtual GlyphFactory();virtual Character* createCharacter (char) :virtual Row* CreateRow ( ) ;virtual column* createcolumn () ;// ...
private:character* _character [NCHARCODES];
);
_character数组包含一些指针,指向以字母代码为索引的Character Glyphs。该数组在构造函数中被初始化为零。
GlyphFactory::GlyphFactory(){
for (int i =0;i < NCHARCODES;++i){_character[i]=0;
}
CreateCharacter在字母符号数组中查找一个字符,如果存在的话,返回相应的Glyph。若不存在,CreateCharacter就创建一个Glyph,将其放入数组中,并返回它:
Character*, GlyphFactory::Createcharacter (char c){if (!_character[c]){_characteri[c] = new Character(c);}return _character[c] ;
}
其他操作仅需在每次被调用时实例化一个新对象,因为非字符的Glyph不能被共享:
Row* GlyphFactory::createRow(){return new Row;
}
column* GlyphFactory::createcolumn(){return new column;
)
我们可以忽略这些操作,让用户直接实例化非共享的Glyph。然而,如果我们想让这些符号以后可以被共享,必须改变创建它们的客户程序代码。
10.相关模式
Flyweight模式通常和Composite(4.3)模式结合起来,用共享叶结点的有向无环图实现一个逻辑上的层次结构。
通常,最好用Flyweight实现State(5.8)和Strategy(5.9)对象。
4.7 PROXY(代理)
1.意图
为其他对象提供一种代理以控制对这个对象的访问。
2.别名
Surrogate
3.动机
对一个对象进行访问控制的一个原因是为了只有在我们确实需要这个对象时才对它进行创建和初始化。我们考虑一个可以在文档中嵌入图形对象的文档编辑器。有些图形对象(如大型光栅图像)的创建开销很大。但是打开文档必须很迅速,因此我们在打开文档时应避免一次性创建所有开销很大的对象。因为并非所有这些对象在文档中都同时可见,所以也没有必要同时创建这些对象。
这一限制条件意味着,对于每一个开销很大的对象,应该根据需要进行创建,当一个图像变为可见时会产生这样的需要。但是在文档中我们用什么来代替这个图像呢?我们又如何才能隐藏根据需要创建图像这一事实,从而不会使得编辑器的实现复杂化呢?例如,这种优化不应影响绘制和格式化的代码。
问题的解决方案是使用另一个对象,即图像Proxy,替代那个真正的图像。Proxy可以代替一个图像对象,并且在需要时负责实例化这个图像对象。
只有当文档编辑器激活图像代理的Draw操作以显示这个图像的时候,图像Proxy才创建真正的图像。Proxy直接将随后的请求转发给这个图像对象。因此在创建这个图像以后,它必须有一个指向这个图像的引用。
我们假设图像存储在一个独立的文件中。这样我们可以把文件名作为对实际对象的引用。Proxy还存储了图像的尺寸( extent),即它的长和宽。有了图像尺寸,Proxy无须真正实例化这个图像就可以响应格式化程序对图像尺寸的请求。
以下的类图更详细地阐述了这个例子。
文档编辑器通过抽象的Graphic类定义的接口访问嵌入的图像。ImageProxy是那些根据需要创建的图像的类,ImageProxy保存了文件名作为指向磁盘上的图像文件的指针。该文件名被作为一个参数传递给ImageProxy的构造器。
ImageProxy还存储了这个图像的边框以及对真正的Image实例的指引,直到代理实例化真正的图像时,这个指引才有效。Draw操作必须保证在向这个图像转发请求之前,它已经被实例化了。GetExtent操作只有在图像被实例化后才向它传递请求,否则,ImageProxy返回它存储的图像尺寸。
4.适用性
在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用Proxy模式。下面是一些可以使用Proxy模式常见情况:
- 远程代理( Remote Proxy)
为一个对象在不同的地址空间提供局部代表。NEXTSTEP使用NXProxy类实现了这一目的。Coplien称这种代理为“大使”( Ambassador )
- 虚代理(Virtual Proxy)
根据需要创建开销很大的对象。在动机一节描述的ImageProxy就是这样一种代理的例子。
- 保护代理(Protection Proxy)
控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。例如,在Choices操作系统[CIRM93]中KemelProxies为操作系统对象提供了访问保护。
- 智能指引 (Smart Reference)
取代了简单的指针,它在访问对象时执行一些附加操作。它的典型用途包括:
- 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为SmartPointers)。
- 当第一次引用一个持久对象时,将它装入内存。
- 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
5.结构
这是运行时刻一种可能的代理结构的对象图。
6.参与者
- Proxy (ImageProxy)
- 保存一个引用使得代理可以访问实体。若RealSubject和Subject的接口相同,Proxy会
引用Subject。 - 提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体。一控制对实体的存取,并可能负责创建和删除它。
- 其他功能依赖于代理的类型:
- Remote Proxy负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求。‘
- Virtual Proxy可以缓存实体的附加信息,以便延迟对它的访问。例如,动机一节中提到的ImageProxy缓存了图像实体的尺寸。
- Protection Proxy检查调用者是否具有实现一个请求所必需的访问权限。
- 保存一个引用使得代理可以访问实体。若RealSubject和Subject的接口相同,Proxy会
- Subject (Graphic)
- 定义RealSubject 和Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxya
- RealSubject (Image)
- 定义Proxy所代表的实体。
7.协作
- 代理根据其种类,在适当的时候向RealSubject转发请求。
8.效果
Proxy模式在访问对象时引入了一定程度的间接性。根据代理的类型,附加的间接性有多种用途:
- Remote Proxy可以隐藏一个对象存在于不同地址空间的事实。
- Virtual Proxy可以进行最优化,例如根据要求创建对象。
- Protection Proxies和Smart Reference都允许在访问—个对象时有一些附加的内务处理( Housekeeping task )。
Proxy模式还可以对用户隐藏另一种称之为copy-on-write的优化方式,该优化与根据需要创建对象有关。拷贝一个庞大而复杂的对象是一种开销很大的操作,如果这个拷贝根本没有被修改,那么这些开销就没有必要。用代理延迟这一拷贝过程,我们可以保证只有当这个对象被修改的时候才对它进行拷贝。
在实现Copy-on-write时必须对实体进行引用计数。拷贝代理仅会增加引用计数。只有当用户请求一个修改该实体的操作时,代理才会真正的拷贝它。在这种情况下,代理还必须减少实体的引用计数。当引用的数目为零时,这个实体将被删除。
Copy-on-Write可以大幅度的降低拷贝庞大实体时的开销。
9.实现
Proxy模式可以利用以下一些语言特性:
- 重载C++中的存取运算符
C++支持重载运算符->。重载这一运算符使你可以在撤消对一个对象的引用时,执行一些附加的操作。这一点可以用于实现某些种类的代理;代理的作用就象一个指针。
下面的例子说明怎样使用这一技术实现一个称为ImagePtr的虚代理。
claaa Image;
extern Image* LoadAnImageFile(conat char*);
// external function
class ImagePtr{
public:ImagePtr(const char* imageFile);virtual ~ImagePtr():virtual Image* operator->();virtual Image& operator*();
private:Image* LoadImage();
private:Image* _image;conat char* _imageFile;
};
ImagePtr::ImagePtr(const char* theImageFile){_imageFi1e = theImageFile;_image = 0;
}
Image* ImagePtr::LoadImage(){if(_image == 0){_image = LoadAnImageFile(_imageFile);}return _image;
}
重载的>和*运算符使用LoadImage将_image返回给它的调用者(如果必要的话装入它)。
Image* ImagePtr::operator->(){return LoadImage();
}
Image& ImagePtr: : operator*(){return *LoadImage();
}
该方法使你能够通过ImagePtr对象调用Image操作,而省去了把这些操作作为ImagePtr接口的一部分的麻烦。
ImagePtr image= ImagePtr("anImageFileNane");
image->Draw(Point (50,100));// (image.operator->())->Draw(Point (50,100))
请注意这里的image代理起到一个指针的作用,但并没有将它定义为一个指向Image的指针。这意味着你不能把它当作一个真正的指向Image的指针来使用。因此在使用此方法时用户应区别对待Image对象和Imageptr对象。
重载成员访问运算符并非对每一种代理来说都是好办法。有些代理需要清楚地知道调用了都个操作,重载运算符的方法在这种情况下行不通。
考虑在目的一节提到的虚代理的例子,图像应该在一个特定的时刻被装载——也就是在Draw操作被调用时——而不是在只要引用这个图像就装载它。重载访问操作符不能作出这种区分。在这种情况下我们只能人工实现每一个代理操作,向实体转发请求。
正如示例代码中所示的那样,这些操作之间非常相似。一般来说,所有的操作在向实体转发请求之前,都要检验这个要求是否合法,原始对象是否存在等。但重复写这些代码很麻烦,因此我们一般用一个预处理程序自动生成它。
- 使用Smalltalk中的doesNotUnderstand
Smalltalk提供一个hook方法可以用来自动转发请求。当用户向接受者发送一个消息,但是这个接受者没有相关方法的时候,Samlltalk调用方法doesNotUnderstand: amessage。Proxy类可以重定义doesNotUnderstand以便向它的实体转发这个消息。
为了保证一个请求真正被转发给实体,而不是无声无息的被代理所吸收,我们可以定义一个不理解任何信息的Proxy类。Smalltalk定义了一个没有任何超类的Proxy类,实现了这个目的。
doesNotUnderstand:的主要缺点在于:大多数Smalltalk系统都有一些由虚拟机直接控制的特殊消息,而这些消息并不引起通常的方法查找。唯一一个通常用Object实现(因而可以影响代理)的符号是恒等运算符= =。
如果你准备使用doesNotUnderstand:来实现Proxy的话,你必须围绕这一问题进行设计。对代理的标识并不意味着对真正实体的标识。doesNotUnderstand:另一个缺点是,它主要用作错误处理,而不是创建代理,因此一般来说它的速度不是很快。
- Proxy并不总是需要知道实体的类型
若Proxy类能够完全通过一个抽象接口处理它的实体,则无须为每一个RealSubject类都生成一个Proxy类;Proxy可以统一处理所有的RealSubject类。但是如果Proxy要实例化RealSubjects (例如在virtual proxy中),那么它们必须知道具体的类。
另一个实现方面的问题涉及到在实例化实体以前怎样引用它。有些代理必须引用它们的实体,无论它是在硬盘上还是在内存中。这意味着它们必须使用某种独立于地址空间的对象标识符。在目的一节中,我们采用一个文件名来实现这种对象标识符。
10.代码示例
以下代码实现了两种代理:在目的一节描述的Virtual Proxy,和用doesNotUnderstand:实现的Proxy。
- Virtual Proxy Graphic类为图形对象定义一个接口。
class Graphic {
public:virtual ~Graphic();virtual void Draw (const Point& at) = 0;virtual void HandleMouse (Event& event) = 0;virtual const Point& GetExtent () = 0;virtual void Load(istream& from) = 0;virtual void save(ostream& to) =0;
protected:Graphic();
} ;
Image类实现了Graphic接口用来显示图像文件。Image重定义Handlemouse操作,使得用户可以交互的调整图像的尺寸。
class Image:public Graphic {
public:Image(const char* file); // loads image from a filevirtual ~Image( ) ;virtual void Draw (const Point& at) ;virtual void HandleMouse (Event& event);virtual const Point& GetExtent () ;virtual void Load (istream& from) ;virtual void Save (ostream& to) ;
private://...
};
ImageProxy和Image具有相同的接口:
class ImageProxy:public Graphic {
public:ImageProxy (const char* imageFile) ;virtual ~ImageProy();virtual void Draw (const Point& at);virtual void HandleMouse(Event& event);virtual const Point& GetExtent ();virtual void Load(istream& from);virtual void Save (ostream& to);
protected:Image*GetImage( ) ;
private:Image* _image;Point _extent;char* fi1eName ;
};
构造函数保存了存储图像的文件名的本地拷贝,并初始化_extent和_image:
ImageProxy::ImageProxy (const char* fileName){_fileName = strdup (fileName);_extent = Point::zero; // don't know extent yet_image = 0;
}Image* ImageProxy::GetImage(){if(_image == 0){_image = new Image(_fileName) ;}return _image;
}
如果可能的话,GetExtent的实现部分返回缓存的图像尺寸,否则从文件中装载图像。Draw用来装载图像,HandelMouse则向实际图像转发这个事件。
const Point& ImageProxy::GetExtent(){if(_extent == Point:: zero){_extent = GetImage()->GetExtent();}return _extent;
}void ImageProxy::Draw(const Point& at){GetImage()->Draw(at);
}void ImageProxy::HandleMouse(Event& event){GetImage()->HandleMouse(event);
}
Save操作将缓存的图像尺寸和文件名保存在一个流中。Load得到这个信息并初始化相应的成员函数。
void ImageProxy::Save(iostream& to) {to << _extent << _fileName;
}
void ImageProxcy::Load(fiatream& from){from >> _extent >> _fileName;
}
最后,假设我们有一个类TextDocument能够包含Graphic对象:
class TextDocument{
public:TextDocument();void Insert(Graphic*);// ...
};
我们可以用以下方式把ImageProxy插入到文本文件中。
TextDocument* text = new TextDoCument;
// ...
text->Insert(new ImageProxy ( "anImageFileName" ));
- 使用doesNotUnderstand的Proxy
在Smalltalk中,你可以定义超类为nil的类,同时定义doesNotUnderstand:方法处理消息,这样构建一些通用的代理。
在以下程序中我们假设代理有一个realSubject方法,该方法返回它的实体。在ImageProxy中,该方法将检查是否已创建了Image,并在必要的时候创建它,最后返回Image。它使用perform: withArguments:来执行被保留在实体中的那些消息。
doesNottnderstand: aMessage^ self realSubjectperform: aMessage selectorwithArguments: aMessage arguments
doesNotUnderstand:的参数是Message的一个实例,它表示代理不能理解的消息。所以,代理在转发消息给实体之前,首先确定实体的存在性,并由此对所有的消息做出响应。
doesNotUnderstand:的一个优点是它可以执行任意的处理过程。例如,我们可以用这样的方式生成一个protection proxy,即指定一个可以接受的消息的集合legalMessages,然后给这个代理定义以下方法。
doesNotUnderstand: aMessage^ (legalMessages includes: aMessage selector)ifTrue: [self realSubjectperform: aMessage selectorwithArguments: aMessage arguments]ifFalse: [self error: 'Illegal operator' ]
这个方法在向实体转发一个消息之前,检查它的合法性。如果不是合法的,那么发送error:给代理,除非代理定义error:,这将产生一个错误的无限循环。因此,error:的定义应该同所有它用到的方法一起从Object类中拷贝。
11.相关模式
Adapter(4.1):适配器Adapter为它所适配的对象提供了一个不同的接口。相反,代理提供了与它的实体相同的接口。然而,用于访问保护的代理可能会拒绝执行实体会执行的操作,因此,它的接口实际上可能只是实体接口的一个子集。
Decorator(4.4):尽管decorator的实现部分与代理相似,但decorator的目的不一样。Decorator为对象添加一个或多个功能,而代理则控制对对象的访问。代理的实现与decorator的实现类似,但是在相似的程度上有所差别。
- Protection Proxy的实现可能与decorator的实现差不多。
- Remote Proxy不包含对实体的直接引用,而只是一个间接引用,如“主机ID,主机上的局部地址。”Virtual Proxy开始的时候使用一个间接引用,例如一个文件名,但最终将获取并使用一个直接引用。
资料
[1]《设计模式:可复用面向对象软件的基础》(美) Erich Gamma Richard Helm、Ralph Johnson John Vlissides 著 ; 李英军、马晓星、蔡敏、刘建中 等译; 吕建 审校