0%

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。

享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

享元模式的主要优点是:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

其主要缺点是:

  • 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  • 读取享元模式的外部状态会使得运行时间稍微变长。

享元模式中存在以下两种状态:

  • 内部状态,即不会随着环境的改变而改变的可共享部分。
  • 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

模式的结构#

享元模式的主要角色有如下:

  • 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  • 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
  • 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中
  • 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

下图是享元模式的结构图。图中的 UnsharedConcreteFlyweight 是非享元角色,里面包含了非共享的外部状态信息 info;而 Flyweight 是抽象享元角色,里面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部状态以参数的形式通过该方法传入;ConcreteFlyweight 是具体享元角色,包含了关键字 key,它实现了抽象享元接口;FlyweightFactory 是享元工厂角色,它逝关键字 key 来管理具体享元;客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法。

模式的实现#

享元模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class Program
{
static void Main(string[] args)
{
FlyweightFactory factory=new FlyweightFactory();
IFlyweight f01=factory.GetFlyweight("a");
IFlyweight f02=factory.GetFlyweight("a");
IFlyweight f03=factory.GetFlyweight("a");
IFlyweight f11=factory.GetFlyweight("b");
IFlyweight f12=factory.GetFlyweight("b");
f01.Operation(new UnsharedConcreteFlyweight("第1次调用a。"));
f02.Operation(new UnsharedConcreteFlyweight("第2次调用a。"));
f03.Operation(new UnsharedConcreteFlyweight("第3次调用a。"));
f11.Operation(new UnsharedConcreteFlyweight("第1次调用b。"));
f12.Operation(new UnsharedConcreteFlyweight("第2次调用b。"));
Console.ReadLine();
}
}


public class UnsharedConcreteFlyweight
{
private String info;
public UnsharedConcreteFlyweight(String info)
{
this.info=info;
}
public String GetInfo()
{
return info;
}
public void SetInfo(String info)
{
this.info=info;
}
}


public interface IFlyweight
{
void Operation(UnsharedConcreteFlyweight state);
}


public class ConcreteFlyweight : IFlyweight
{
private String key;
public ConcreteFlyweight(String key)
{
this.key=key;
Console.WriteLine("具体享元"+key+"被创建!");
}
public void Operation(UnsharedConcreteFlyweight outState)
{
Console.WriteLine("具体享元"+key+"被调用,");
Console.WriteLine("非享元信息是:" + outState.GetInfo());
}
}


public class FlyweightFactory
{
private Dictionary<String, IFlyweight> flyweights = new Dictionary<String, IFlyweight>();
public IFlyweight GetFlyweight(String key)
{
IFlyweight flyweight;
flyweights.TryGetValue(key,out flyweight);
if(flyweight!=null)
{
Console.WriteLine("具体享元" + key + "已经存在,被成功获取!");
}
else
{
flyweight=new ConcreteFlyweight(key);
flyweights.Add(key, flyweight);
}
return flyweight;
}
}

程序运行结果如下:

1
2
3
4
5
6
7
8
9
10
具体享元a被创建!
具体享元a已经存在,被成功获取!
具体享元a已经存在,被成功获取!
具体享元b被创建!
具体享元b已经存在,被成功获取!
具体享元a被调用,非享元信息是:第1次调用a。
具体享元a被调用,非享元信息是:第2次调用a。
具体享元a被调用,非享元信息是:第3次调用a。
具体享元b被调用,非享元信息是:第1次调用b。
具体享元b被调用,非享元信息是:第2次调用b。

享元模式是通过减少内存中对象的数量来节省内存空间的,所以以下几种情形适合采用享元模式:

  • 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
  • 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
  • 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式

在前面介绍的享元模式中,其结构图通常包含可以共享的部分和不可以共享的部分。在实际使用过程中,有时候会稍加改变,即存在两种特殊的享元模式:单纯享元模式和复合享元模式,下面分别对它们进行简单介绍。

  • 单纯享元模式:这种享元模式中的所有的具体享元类都是可以共享的,不存在非共享的具体享元类,其结构图如图所示:

  • 复合享元模式:这种享元模式中的有些享元对象是由一些单纯享元对象组合而成的,它们就是复合享元对象。虽然复合享元对象本身不能共享,但它们可以分解成单纯享元对象再被共享,其结构图如图所示:

在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介

主要优点有:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
  • 代理对象可以扩展目标对象的功能
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度

主要缺点有:

  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢
  • 增加了系统的复杂度

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问。

模式的结构#

代理模式的主要角色如下:

  • 抽象主题(Subject)类:通过接口或抽象类(推荐使用接口)声明真实主题和代理对象实现的业务方法
  • 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能

其结构图如图所示:

模式的实现#

代理模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

class Program
{
static void Main(string[] args)
{

Proxy proxy=new Proxy();
proxy.Request();
Console.ReadKey();
}
}


public interface ISubject
{
void Request();
}


public class RealSubject :ISubject
{
public void Request()
{
Console.WriteLine("访问真实主题方法...");
}
}


public class Proxy : ISubject
{
private RealSubject realSubject;
public void Request()
{
if (realSubject==null)
{
realSubject=new RealSubject();
}
PreRequest();
realSubject.Request();
PostRequest();
}
public void PreRequest()
{
Console.WriteLine("访问真实主题之前的预处理。");
}
public void PostRequest()
{
Console.WriteLine("访问真实主题之后的后续处理。");
}
}

程序运行的结果如下:

1
2
3
访问真实主题之前的预处理。
访问真实主题方法...
访问真实主题之后的后续处理。

前面分析了代理模式的结构与特点,现在来分析以下的应用场景:

  • 远程代理(Remote Proxy):这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。
    例如,用户访问网盘的虚拟硬盘时实际访问的是网盘空间。
  • 虚拟代理(Virtual Proxy):这种方式通常用于要创建的目标对象开销很大时。
    例如,下载一幅很大的图像需要很长时间,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
  • 保护代理(Protection Proxy):这种方式通常用于控制不同种类客户对真实对象的访问权限。
  • 智能指引(Smart Reference):主要用于调用目标对象时,代理附加一些额外的处理功能。

智能指引的典型用途包括:

  • 增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它;
  • 当第一次引用一个持久对象时,将它装入内存。
  • 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。

在前面介绍的代理模式中,代理类中包含了对真实主题的引用,这种方式存在两个缺点:

  • 真实主题与代理主题一一对应,增加真实主题也要增加代理
  • 设计代理以前真实主题必须事先存在,不太灵活。

采用动态代理模式可以解决以上问题(如 SpringAOP),C#中可以使用RealProxy实现动态代理,有两种方法:
第一种:只使用RealProxy,不能代理带out参数的方法(可能是我没找到),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***\r\n Begin program - logging with decorator\r\n");
IRepository<Customer> customerRepository =RepositoryFactory.Create<Customer>();
var customer = new Customer()
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nEnd program - logging with decorator\r\n***");
Console.ReadLine();
}
}

public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}


public interface IRepository<T>
{
void Add(T entity);
void Delete(T entity);
void Update(T entity);
IEnumerable<T> GetAll();
T GetById(int id);
}


public class Repository<T> : IRepository<T>
{
public void Add(T entity)
{
Console.WriteLine("Adding {0}", entity);
}
public void Delete(T entity)
{
Console.WriteLine("Deleting {0}", entity);
}
public void Update(T entity)
{
Console.WriteLine("Updating {0}", entity);
}
public IEnumerable<T> GetAll()
{
Console.WriteLine("Getting entities");
return null;
}
public T GetById(int id)
{
Console.WriteLine("Getting entity {0}", id);
return default(T);
}
}


class DynamicProxy<T> : RealProxy
{
private readonly T _decorated;
public DynamicProxy(T decorated) : base(typeof(T))
{
_decorated = decorated;
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
Log("In Dynamic Proxy - Before executing '{0}'",methodCall.MethodName);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
Log("In Dynamic Proxy - After executing '{0}' ",methodCall.MethodName);
return new ReturnMessage(result, null, 0,methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
Log(string.Format("In Dynamic Proxy- Exception {0} executing '{1}'", e),methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
}


public class RepositoryFactory
{
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
return dynamicProxy.GetTransparentProxy() as IRepository<T>;
}
}

第二种:使用RealProxy、MarshalByRefObject,可以代理带out参数的方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

public class Program
{
static void Main(string[] args)
{

Proxy<ISubject> proxy = new Proxy<ISubject>(new RealSubject());
ISubject subject = (ISubject)proxy.GetTransparentProxy();
int arg = 0;
subject.Request(out arg);
Console.WriteLine(arg);
Console.ReadKey();
}
}

public class Proxy<T> : RealProxy where T: class
{
MarshalByRefObject myMarshalByRefObject;
public Proxy(MarshalByRefObject realT) : base(typeof(T))
{
myMarshalByRefObject = realT;
}
public override IMessage Invoke(IMessage myMessage)
{
IMethodCallMessage myCallMessage = (IMethodCallMessage)myMessage;
Console.WriteLine("动态代理方法中:执行前");
IMethodReturnMessage myIMethodReturnMessage = RemotingServices.ExecuteMessage(myMarshalByRefObject, myCallMessage);
Console.WriteLine("动态代理方法中:执行后");
return myIMethodReturnMessage;
}
}


public interface ISubject
{
void Request(out int arg);
}


public class RealSubject : MarshalByRefObject,ISubject
{
public void Request(out int arg)
{
arg = 1;
Console.WriteLine("访问真实主题方法...");
}
}

C#中动态代理与泛型函数——CSDN
面向方面的编程-使用 RealProxy 类进行面向方面的编程——MSDN

在现实生活中,常常存在办事较复杂的例子,如办房产证或注册一家公司,有时要同多个部门联系,这时要是有一个综合部门能解决一切手续问题就好了。

软件设计也是这样,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了“开闭原则”,也违背了“迪米特法则”,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式的目标。

客户去当地房产局办理房产证过户要遇到的相关部门:

外观(Facade)模式的定义:是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点:

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
  • 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。

外观(Facade)模式的主要缺点如下:

  • 不能很好地限制客户使用子系统类。
  • 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。

模式的结构#

外观(Facade)模式包含以下主要角色:

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
  • 客户(Client)角色:通过一个外观角色访问各个子系统的功能。

其结构图如图所示:

模式的实现#

外观模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Program
{
static void Main(string[] args)
{
Facade f = new Facade();
f.method();
Console.ReadLine();
}
}


public class Facade
{
private SubSystem01 obj1=new SubSystem01();
private SubSystem02 obj2=new SubSystem02();
private SubSystem03 obj3=new SubSystem03();
public void method()
{
obj1.Method1();
obj2.Method2();
obj3.Method3();
}
}

public class SubSystem01
{
public void Method1()
{
Console.WriteLine("子系统01的Method1()被调用!");
}
}


public class SubSystem02
{
public void Method2()
{
Console.WriteLine("子系统02的Method2()被调用!");
}
}


public class SubSystem03
{
public void Method3()
{
Console.WriteLine("子系统03的Method3()被调用!");
}
}

程序运行结果如下:

1
2
3
子系统01的Method1()被调用!
子系统02的Method2()被调用!
子系统03的Method3()被调用!

通常在以下情况下可以考虑使用外观模式:

  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
  • 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

在外观模式中,当增加或移除子系统时需要修改外观类,这违背了“开闭原则”。如果引入抽象外观类(或接口),则在一定程度上解决了该问题,其结构图如图所示:

在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣月艮与衣柜以及厨房中的锅碗瓢盆等。
在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。

组合(Composite)模式的定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。

组合模式的主要优点有:

  • 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码
  • 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”

其主要缺点是:

  • 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
  • 不容易限制容器中的构件;
  • 不容易用继承的方法来增加构件的新功能;

模式的结构#

组合模式包含以下主要角色:

  • 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。
  • 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。
  • 树枝构件(Composite)角色:是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法

组合模式分为透明式的组合模式和安全式的组合模式

  • 透明方式:在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild() 方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题。其结构图如图所示:

  • 安全方式:在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性。其结构图如图所示:

模式的实现#

假如要访问集合 c0={leaf1,{leaf2,leaf3}} 中的元素,其对应的树状图如图所示:

下面给出透明式的组合模式的实现代码,安全式的组合模式与之类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Program
{
static void Main(string[] args)
{
IComponent c0=new Composite();
IComponent c1=new Composite();
IComponent leaf1=new Leaf("1");
IComponent leaf2=new Leaf("2");
IComponent leaf3=new Leaf("3");
c0.Add(leaf1);
c0.Add(c1);
c1.Add(leaf2);
c1.Add(leaf3);
c0.Operation();
Console.Read();
}
}


public interface IComponent
{
void Add(IComponent c);
void Remove(IComponent c);
IComponent GetChild(int i);
void Operation();
}


public class Leaf : IComponent
{
private String name;
public Leaf(String name)
{
this.name=name;
}
public void Add(IComponent c){ }
public void Remove(IComponent c){ }
public IComponent GetChild(int i)
{
return null;
}
public void Operation()
{
Console.WriteLine("树叶"+name+":被访问!");
}
}


public class Composite : IComponent
{
private List<IComponent> children=new List<IComponent>();
public void Add(IComponent c)
{
children.Add(c);
}
public void Remove(IComponent c)
{
children.Remove(c);
}
public IComponent GetChild(int i)
{
return children[i];
}
public void Operation()
{
foreach (var obj in children)
{
obj.Operation();
}
}
}

程序运行结果如下:

1
2
3
树叶1:被访问!
树叶2:被访问!
树叶3:被访问!

前面分析了组合模式的结构与特点,下面分析它适用的以下应用场景:

  • 在需要表示一个对象整体与部分的层次结构的场合。
  • 要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。

如果对前面介绍的组合模式中的树叶节点和树枝节点进行抽象,也就是说树叶节点和树枝节点还有子节点,这时组合模式就扩展成复杂的组合模式了,如 Java AWT/Swing 中的简单组件 JTextComponent 有子类 JTextField、JTextArea,容器组件 Container 也有子类 Window、Panel。复杂的组合模式的结构图如图所示:

在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等。
在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰模式来实现。

装饰(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式

装饰(Decorator)模式的主要优点有:

  • 采用装饰模式扩展对象的功能比采用继承方式更加灵活
  • 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合

其主要缺点是:装饰模式增加了许多子类,如果过度使用会使程序变得很复杂

通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。

模式的结构

装饰模式主要包含以下角色:

  • 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

装饰模式的结构图如图所示:

模式的实现#

装饰模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Program
{
static void Main(string[] args)
{
IComponent p=new ConcreteComponent();
p.Operation();
Console.WriteLine("---------------------------------");
IComponent d=new ConcreteDecoratorA(p);
d.Operation();
Console.ReadLine();
}
}


public interface IComponent
{
void Operation();
}


public class ConcreteComponent : IComponent
{
public ConcreteComponent()
{
Console.WriteLine("创建具体构件角色");
}
public void Operation()
{
Console.WriteLine("调用具体构件角色的方法Operation()");
}
}


public abstract class Decorator : IComponent
{
private IComponent component;
public Decorator(IComponent component)
{
this.component=component;
}
public virtual void Operation()
{
component.Operation();
}
}


public class ConcreteDecoratorA : Decorator
{
public ConcreteDecoratorA(IComponent component)
: base(component)
{

}
public override void Operation()
{
base.Operation();
AddedFunction();
}
public void AddedFunction()
{
Console.WriteLine("为具体构件角色增加额外的功能AddedFunction()");
}
}

程序运行结果如下:

1
2
3
4
5
创建具体构件角色
调用具体构件角色的方法Operation()
---------------------------------
调用具体构件角色的方法Operation()
为具体构件角色增加额外的功能AddedFunction()

装饰模式通常在以下几种情况使用:

  • 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
  • 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

Java的java.io包和.NET的Stream类都使用了该模式。我不会就它们的实现谈论很多细节。.NET中,Stream是一个抽象类,提供了基本的行为(如Component),从抽象类Stream继承来的类FileStream,MemoryStream是ConcreteCompents,类BufferedStream,CryptoStream等是ConcreteDecorators。你能清楚地认识到它们同样忽略了Decorator

装饰模式所包含的 4 个角色不是任何时候都要存在的,在有些应用环境下模式是可以简化的,如以下两种情况:

  • 如果只有一个具体构件而没有抽象构件时,可以让抽象装饰继承具体构件,其结构图如图所示:

  • 如果只有一个具体装饰时,可以将抽象装饰和具体装饰合并,其结构图如图所示:

在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,用直流电的笔记本电脑接交流电源时需要一个电源适配器。
在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题。

适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

该模式的主要优点如下:

  • 客户端通过适配器可以透明地调用目标接口
  • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。

其缺点是:对类适配器来说,更换适配器的实现过程比较复杂

类适配器模式可采用多重继承方式实现,如 C++ 可定义一个适配器类来同时继承当前系统的业务接口和现有组件库中已经存在的组件接口;C#、Java 不支持多继承,但可以定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。

模式的结构#

适配器模式(Adapter)包含以下主要角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者

类适配器模式的结构图如图所示:

对象适配器模式的结构图如图所示:

模式的实现#

类适配器模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

public class Program
{
static void Main(string[] args)
{

Console.WriteLine("类适配器模式测试:");
ITarget target = new ClassAdapter();
target.Request();
Console.ReadLine();
}
}


public interface ITarget
{
void Request();
}


public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("适配者中的业务代码被调用!");
}
}


public class ClassAdapter : Adaptee,ITarget
{
public void Request()
{
SpecificRequest();
}
}

程序的运行结果如下:

1
2
类适配器模式测试:
适配者中的业务代码被调用!

对象适配器模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

public class Program
{
static void Main(string[] args)
{

Console.WriteLine("类适配器模式测试:");
Adaptee adaptee = new Adaptee();
ITarget target = new ObjectAdapter(adaptee);
target.Request();
Console.ReadLine();
}
}


class ObjectAdapter : ITarget
{
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee)
{
this.adaptee=adaptee;
}
public void Request()
{
adaptee.SpecificRequest();
}
}

说明:对象适配器模式中的“目标接口”和“适配者类”的代码同类适配器模式一样,只要修改适配器类和客户端的代码即可。

程序的运行结果如下:

1
2
类适配器模式测试:
适配者中的业务代码被调用!

适配器模式(Adapter)通常适用于以下场景:

  • 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。

适配器模式(Adapter)可扩展为双向适配器模式,双向适配器类既可以把适配者接口转换成目标接口,也可以把目标接口转换成适配者接口,其结构图如图所示:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

public class Program
{
static void Main(string[] args)
{

Console.WriteLine("类适配器模式测试:");
ITwoWayTarget target = new TargetRealize();
ITwoWayAdaptee adaptee = new AdapteeRealize();
Console.WriteLine("目标通过双向适配器访问适配者:");
ITwoWayTarget twoWayTarget = new TwoWayAdapter(adaptee);
twoWayTarget.Request();
Console.WriteLine("-------------------");
Console.WriteLine("适配者通过双向适配器访问目标:");
ITwoWayAdaptee twoWayAdaptee = new TwoWayAdapter(target);
twoWayAdaptee.SpecificRequest();
Console.ReadLine();
}
}


public interface ITwoWayTarget
{
void Request();
}


public interface ITwoWayAdaptee
{
void SpecificRequest();
}


public class TargetRealize :ITwoWayTarget
{
public void Request()
{
Console.WriteLine("目标代码被调用!");
}
}


public class AdapteeRealize : ITwoWayAdaptee
{
public void SpecificRequest()
{
Console.WriteLine("适配者代码被调用!");
}
}


public class TwoWayAdapter : ITwoWayTarget, ITwoWayAdaptee
{
private ITwoWayTarget target;
private ITwoWayAdaptee adaptee;
public TwoWayAdapter(ITwoWayTarget target)
{
this.target=target;
}
public TwoWayAdapter(ITwoWayAdaptee adaptee)
{
this.adaptee=adaptee;
}
public void Request()
{
adaptee.SpecificRequest();
}
public void SpecificRequest()
{
target.Request();
}
}

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。
如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。
例如,你刚刚参力口工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。
在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。
所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。

中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用

中介者模式是一种对象行为型模式,其主要优点如下:

  • 降低了对象之间的耦合性,使得对象易于独立地被复用。
  • 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

其主要缺点是:当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

模式的结构

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

中介者模式的结构图如图所示:

模式的实现

中介者模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class Program
{
static void Main(string[] args)
{
Mediator md=new ConcreteMediator();
Colleague c1,c2;
c1=new ConcreteColleague1();
c2=new ConcreteColleague2();
md.Register(c1);
md.Register(c2);
c1.Send();
Console.WriteLine("-------------");
c2.Send();

Console.Read();
}
}


public abstract class Mediator
{

public abstract void Register(Colleague colleague);

public abstract void Relay(Colleague colleague);
}


public class ConcreteMediator : Mediator
{
private List<Colleague> colleagues=new List<Colleague>();
public override void Register(Colleague colleague)
{
if(!colleagues.Contains(colleague))
{
colleagues.Add(colleague);
colleague.SetMediator(this);
}
}
public override void Relay(Colleague colleague)
{
foreach (var item in colleagues)
{
if (!item.Equals(colleague))
{
item.Receive();
}
}
}
}


public abstract class Colleague
{
protected Mediator mediator;
public void SetMediator(Mediator mediator)
{
this.mediator=mediator;
}
public abstract void Receive();
public abstract void Send();
}


public class ConcreteColleague1 : Colleague
{
public override void Receive()
{
Console.WriteLine("具体同事类1收到请求。");
}
public override void Send()
{
Console.WriteLine("具体同事类1发出请求。");
mediator.Relay(this);
}
}


public class ConcreteColleague2 : Colleague
{
public override void Receive()
{
Console.WriteLine("具体同事类2收到请求。");
}
public override void Send()
{
Console.WriteLine("具体同事类2发出请求。");
mediator.Relay(this);
}
}

程序的运行结果如下:

1
2
3
4
5
具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

前面分析了中介者模式的结构与特点,下面分析其以下应用场景:

  • 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

在实际开发中,通常采用以下两种方法来简化中介者模式,使开发变得更简单:

  • 不定义中介者接口,把具体中介者对象实现成为单例。
  • 同事对象不持有中介者,而是在需要的时直接获取中介者对象并调用。

下图所示是简化中介者模式的结构图:

程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class Program
{
static void Main(string[] args)
{
ISimpleColleague c1,c2;
c1=new SimpleConcreteColleague1();
c2=new SimpleConcreteColleague2();
c1.Send();
Console.WriteLine("-----------------");
c2.Send();
Console.Read();
}
}


public class SimpleMediator
{
private static SimpleMediator smd=new SimpleMediator();
private List<ISimpleColleague> colleagues=new List<ISimpleColleague>();
private SimpleMediator(){}
public static SimpleMediator GetMediator()
{
return smd;
}
public void Register(ISimpleColleague colleague)
{
if(!colleagues.Contains(colleague))
{
colleagues.Add(colleague);
}
}
public void Relay(ISimpleColleague scl)
{
foreach (var item in colleagues)
{
if (!item.Equals(scl))
{
item.Receive();
}
}
}
}

public interface ISimpleColleague
{
void Receive();
void Send();
}


public class SimpleConcreteColleague1 : ISimpleColleague
{
public SimpleConcreteColleague1()
{
SimpleMediator smd=SimpleMediator.GetMediator();
smd.Register(this);
}
public void Receive()
{
Console.WriteLine("具体同事类1:收到请求。");
}
public void Send()
{
SimpleMediator smd=SimpleMediator.GetMediator();
Console.WriteLine("具体同事类1:发出请求...");
smd.Relay(this);
}
}


public class SimpleConcreteColleague2 : ISimpleColleague
{
public SimpleConcreteColleague2()
{
SimpleMediator smd=SimpleMediator.GetMediator();
smd.Register(this);
}
public void Receive()
{
Console.WriteLine("具体同事类2:收到请求。");
}
public void Send()
{
SimpleMediator smd=SimpleMediator.GetMediator();
Console.WriteLine("具体同事类2:发出请求...");
smd.Relay(this);
}
}

程序运行结果如下:

1
2
3
4
5
具体同事类1:发出请求...
具体同事类2:收到请求。
-----------------
具体同事类2:发出请求...
具体同事类1:收到请求。

在软件开发系统中,常常出现“方法的请求者”与“方法的实现者”之间存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对行为进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与方法的实现者解耦?”变得很重要,命令模式能很好地解决这个问题。

命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。

命令模式的主要优点如下:

  • 降低系统的耦合度:命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命令非常方便:采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令:命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作:命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

其缺点是:可能产生大量具体命令类。因为计对每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。

可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。

模式的结构#

命令模式包含以下主要角色:

  • 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
  • 具体命令角色(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作
  • 实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
  • 调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。

其结构图如图所示:

模式的实现#

命令模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Program
{
static void Main(string[] args)
{
ICommand cmd=new ConcreteCommand();
Invoker ir=new Invoker(cmd);
Console.WriteLine("客户访问调用者的Call()方法...");
ir.Call();
Console.Read();

}
}


public class Invoker
{
private ICommand command;
public Invoker(ICommand command)
{
this.command=command;
}
public void SetCommand(ICommand command)
{
this.command=command;
}
public void Call()
{
Console.WriteLine("调用者执行命令command...");
command.Execute();
}
}


public interface ICommand
{
void Execute();
}


public class ConcreteCommand : ICommand
{
private Receiver receiver;
public ConcreteCommand()
{
receiver=new Receiver();
}
public void Execute()
{
receiver.Action();
}
}


public class Receiver
{
public void Action()
{
Console.WriteLine("接收者的Action()方法被调用...");
}
}

程序的运行结果如下:

1
2
3
客户访问调用者的Call()方法...
调用者执行命令command...
接收者的Action()方法被调用...

命令模式通常适用于以下场景:

  • 当系统需要将请求调用者与请求接收者解耦时,命令模式使得调用者和接收者不直接交互。
  • 当系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现这些功能。
  • 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
  • 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。

在软件开发中,有时将命令模式与前面学的组合模式联合使用,这就构成了宏命令模式,也叫组合命令模式。宏命令包含了一组命令,它充当了具体命令与调用者的双重角色,执行它时将递归调用它所包含的所有命令,其具体结构图如图所示:

程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class Program
{
static void Main(string[] args)
{
IAbstractCommand cmd1=new ConcreteCommand1();
IAbstractCommand cmd2=new ConcreteCommand2();
CompositeInvoker ir=new CompositeInvoker();
ir.Add(cmd1);
ir.Add(cmd2);
Console.WriteLine("客户访问调用者的Execute()方法...");
ir.Execute();
Console.Read();

}
}


public interface IAbstractCommand
{
void Execute();
}


public class ConcreteCommand1 : IAbstractCommand
{
private CompositeReceiver receiver;
public ConcreteCommand1()
{
receiver=new CompositeReceiver();
}
public void Execute()
{
receiver.Action1();
}
}


public class ConcreteCommand2 : IAbstractCommand
{
private CompositeReceiver receiver;
public ConcreteCommand2()
{
receiver=new CompositeReceiver();
}
public void Execute()
{
receiver.Action2();
}
}


public class CompositeInvoker : IAbstractCommand
{
private List<IAbstractCommand> children = new List<IAbstractCommand>();
public void Add(IAbstractCommand c)
{
children.Add(c);
}
public void Remove(IAbstractCommand c)
{
children.Remove(c);
}
public IAbstractCommand GetChild(int i)
{
return children[i];
}
public void Execute()
{
foreach (var child in children)
{
child.Execute();
}
}
}


public class CompositeReceiver
{
public void Action1()
{
Console.WriteLine("接收者的Action1()方法被调用...");
}
public void Action2()
{
Console.WriteLine("接收者的Action2()方法被调用...");
}
}

程序的运行结果如下:

1
2
3
客户访问调用者的Execute()方法...
接收者的Action1()方法被调用...
接收者的Action2()方法被调用...

当然,命令模式还可以同备忘录(Memento)模式组合使用,这样就变成了可撤销的命令模式,这将在后面介绍。

每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由“备忘录模式”来实现。

其实很多应用软件都提供了这项功能,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。

备忘录(Memento)模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态,该模式又叫快照模式

备忘录模式是一种对象行为型模式,其主要优点如下:

  • 提供了一种可以恢复状态的机制:当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装:除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类:发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

其主要缺点是:资源消耗大,如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

备忘录模式的核心是设计备忘录类以及用于管理备忘录的管理者类,现在我们来学习其结构与实现。

模式的结构#

备忘录模式的主要角色如下:

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

备忘录模式的结构图如图所示:

模式的实现#

备忘录模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Program
{
static void Main(string[] args)
{
Originator or = new Originator();
Caretaker cr = new Caretaker();
or.SetState("S0");
Console.WriteLine("初始状态:" + or.GetState());
cr.SetMemento(or.CreateMemento());
or.SetState("S1");
Console.WriteLine("新的状态:" + or.GetState());
or.RestoreMemento(cr.GetMemento());
Console.WriteLine("恢复状态:" + or.GetState());
Console.Read();
}
}


public class Memento
{
private String state;
public Memento(String state)
{
this.state=state;
}
public void SetState(String state)
{
this.state=state;
}
public String GetState()
{
return state;
}
}


public class Originator
{
private String state;
public void SetState(String state)
{
this.state=state;
}
public String GetState()
{
return state;
}
public Memento CreateMemento()
{
return new Memento(state);
}
public void RestoreMemento(Memento memento)
{
this.SetState(memento.GetState());
}
}


public class Caretaker
{
private Memento memento;
public void SetMemento(Memento memento)
{
this.memento=memento;
}
public Memento GetMemento()
{
return memento;
}
}

程序运行的结果如下:

1
2
3
初始状态:S0
新的状态:S1
恢复状态:S0

前面学习了备忘录模式的定义与特点、结构与实现,现在来看该模式的以下应用场景:

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

在备忘录模式中,通过定义“备忘录”来备份“发起人”的信息,而原型模式的 clone() 方法具有自备份功能。所以,如果让发起人实现 Cloneable 接口就有备份自己的功能,这时可以删除备忘录类,其结构图如图所示:

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Program
{
static void Main(string[] args)
{
OriginatorPrototype or=new OriginatorPrototype();
PrototypeCaretaker cr=new PrototypeCaretaker();
or.SetState("S0");
Console.WriteLine("初始状态:"+or.GetState());
cr.SetMemento(or.CreateMemento());
or.SetState("S1");
Console.WriteLine("新的状态:"+or.GetState());
or.RestoreMemento(cr.GetMemento());
Console.WriteLine("恢复状态:"+or.GetState());
Console.Read();
}
}


class OriginatorPrototype : ICloneable
{
private String state;
public void SetState(String state)
{
this.state=state;
}
public String GetState()
{
return state;
}
public OriginatorPrototype CreateMemento()
{
return (OriginatorPrototype)this.Clone();
}
public void RestoreMemento(OriginatorPrototype opt)
{
this.SetState(opt.GetState());
}
public object Clone()
{
try
{
return base.MemberwiseClone();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
}

class PrototypeCaretaker
{
private OriginatorPrototype opt;
public void SetMemento(OriginatorPrototype opt)
{
this.opt=opt;
}
public OriginatorPrototype GetMemento()
{
return opt;
}
}

程序的运行结果如下:

1
2
3
初始状态:S0
新的状态:S1
恢复状态:S0

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
例如,一个人每天会起床、吃饭、做事、睡觉等,其中“做事”的内容每天可能不同。我们把这些规定了流程或格式的实例定义成模板,允许使用者根据自己的需求去更新它,例如,简历模板、论文模板、Word 中模板文件等。

模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。

该模式的主要优点如下:

  • 封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  • 它在父类中提取了公共的部分代码,便于代码复用
  • 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则

该模式的主要缺点如下:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度

模板方法模式需要注意抽象类与具体子类之间的协作。它用到了虚函数的多态性技术以及“不用调用我,让我来调用你”的反向控制技术

模式的结构#

模板方法模式包含以下主要角色:

抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成,这些方法的定义如下:

  • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
  • 基本方法:是整个算法中的一个步骤,包含以下几种类型:
  • 抽象方法:在抽象类中申明,由具体子类实现
  • 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它
  • 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种

具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。

模板方法模式的结构图如图所示:

模式的实现#

模板方法模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Program
{
static void Main(string[] args)
{
AbstractClass tm=new ConcreteClass();
tm.TemplateMethod();

Console.Read();
}
}


public abstract class AbstractClass
{
public void TemplateMethod()
{
SpecificMethod();
AbstractMethod1();
AbstractMethod2();
}
public void SpecificMethod()
{
Console.WriteLine("抽象类中的具体方法被调用...");
}
public abstract void AbstractMethod1();
public abstract void AbstractMethod2();
}


public class ConcreteClass : AbstractClass
{
public override void AbstractMethod1()
{
Console.WriteLine("抽象方法1的实现被调用...");
}
public override void AbstractMethod2()
{
Console.WriteLine("抽象方法2的实现被调用...");
}
}

程序的运行结果如下:

1
2
3
抽象类中的具体方法被调用...
抽象方法1的实现被调用...
抽象方法2的实现被调用...

模板方法模式通常适用于以下场景:

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  • 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展

在模板方法模式中,基本方法包含:抽象方法、具体方法和钩子方法,正确使用“钩子方法”可以使得子类控制父类的行为。如下面例子中,可以通过在具体子类中重写钩子方法 HookMethod1() 和 HookMethod2() 来改变抽象父类中的运行结果,其结构图如图所示:

程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Program
{
static void Main(string[] args)
{
HookAbstractClass tm = new HookConcreteClass();
tm.TemplateMethod();

Console.Read();
}
}


public abstract class HookAbstractClass
{
public void TemplateMethod()
{
AbstractMethod1();
HookMethod1();
if(HookMethod2())
{
SpecificMethod();
}
AbstractMethod2();
}
public void SpecificMethod()
{
Console.WriteLine("抽象类中的具体方法被调用...");
}
public virtual void HookMethod1(){}
public virtual bool HookMethod2()
{
return true;
}
public abstract void AbstractMethod1();
public abstract void AbstractMethod2();
}


public class HookConcreteClass : HookAbstractClass
{
public override void AbstractMethod1()
{
Console.WriteLine("抽象方法1的实现被调用...");
}
public override void AbstractMethod2()
{
Console.WriteLine("抽象方法2的实现被调用...");
}
public override void HookMethod1()
{
Console.WriteLine("钩子方法1被重写...");
}
public override bool HookMethod2()
{
return false;
}
}