之前我们已经介绍完了创建型模式,它们分别为Factory Method,Abstract Factory,Singleton,Builder,Prototype。创建型模式是创建对象而不是直接实例化对象,这会使程序在判断给定情况下创建哪个对象时更为灵活。在C#中为new,在Pascal中为Create。
接下来我们要介绍结构型模式,这类模式可以将一组对象组合成更大的结构。对于结构型模式(Structural Pattern)来说主要分为两种组合方式,分别为类模式和对象模式,其主要区别为类模式描述的是如何使用继承提供更有用的程序接口,而对象模式描述的是通过使用对象组合或将对象包含在其他对象里,将对象组合成更大的结构。
下面我们来看看结构型的第一种模式适配器模式(Adapter Pattern)。
适配器模式可以将类的接口转变成客户需要的其他接口。这样做的好处就是能很好的复用已有的代码,比如很多公司都生产数据库,如Oracle,IBM,Microsoft,它们的产品各有各的特点,原生的开发方法也有很大的区别,比如Oracle提供OCI接口,Sql Server提供自己专用的API。如果都使用数据库厂商的原生开发方法,那么在Sql Server开发的软件就很难移植到其他的数据库平台上去,这无疑会限制产品的应用范围。为此很多厂商就提供了各种标准的开发接口,比如最早的ODBC,BDE,到现在的ADO.NET等等,有了这种标准的数据库开发接口,我们在一个平台上开发的产品就很容易移植到其他平台上去。
那么如何让数据库适应不同的标准API而无需大的改动呢,这就是通过适配器模式来实现了,微软最早的ODBC Driver和现在的ADO.NET就相当于一个适配器,可以让数据库适应不同的标准开发接口。这样原有的产品无需针对标准的API做任何改变,只要针对不同的开发API实现一个Dirver就可以了。
适配器模式的实现原理很简单,就是通过实现了客户指定接口的类来响应客户的请求,然后在适配器内部将请求转发给原有的API接口对应的具有类似功能的方法,然后将处理的结果整理后返回给客户。
适配器模式有类的适配器模式和对象的适配器模式两种。我们将分别讨论这两种Adapter模式。
类的Adapter模式的结构:

由图中可以看出,AdapteeXML类没有CreateFile方法,而客户期待这个方法。为了使客户能够使用AdapteeXML类,提供一个中间环节,即类Adapter类,Adapter类实现了ISerTarget接口,此接口中定义了CreateFile方法,并继承自AdapteeXML,Adapter类的CreateFile方法重新封装了AdapteeXMl的CreateXMLFile方法,实现了适配的目的。<?xml:namespace prefix = o />
因为Adapter与AdapteeXML是继承的关系,所以这决定了这个适配器模式是类的。
该适配器模式所涉及的角色包括:
目标(Target)角色:这是客户所期待的接口。因为C#不支持多继承,所以Target必须是接口,不可以是类,这里为ISerTarget。
源(Adaptee)角色:需要适配的类,这里为AdapteeXML。
适配器(Adapter)角色:把源接口转换成目标接口。这一角色必须是类。
以上例子主要是一个XML串行化实例(C#):
1
using System;
2
using System.IO;
3
using System.Xml.Serialization;
4
using System.Xml;
5
namespace AdapterClass
6

{
7
/**//// <summary>
8
///============== Program Description==============
9
///Name:AdapterClass.cs
10
///Objective:AdapterClass
11
///Date:2006-05-04
12
///Written By coffee.liu
13
///================================================
14
/// </summary>
15
class Class1
16
{
17
/**//// <summary>
18
/// 应用程序的主入口点。
19
/// </summary>
20
[STAThread]
21
static void Main(string[] args)
22
{
23
ISerTarget target=new Adapter();
24
target.CreateFile();
25
Console.WriteLine("xml file created");
26
}
27
}
28
[Serializable]
29
public class Person
30
{
31
public string Name;
32
public string Sex;
33
public DateTime Brith;
34
private int PersonID;
35
public Person()
36
{}
37
public void SetID(int ID)
38
{PersonID=ID;}
39
}
40
41
interface ISerTarget
42
{
43
void CreateFile();
44
}
45
class Adapter:AdapteeXML,ISerTarget
46
{
47
public Adapter():base()
48
{
49
}
50
51
public void CreateFile()
52
{
53
this.FillData();
54
this.CreateXMLFile();
55
56
}
57
58
}
59
class AdapteeXML
60
{
61
FileStream aFile;
62
Person aPerson;
63
XmlSerializer aXML;
64
public AdapteeXML()
65
{
66
aXML=new XmlSerializer(typeof(Person));
67
aPerson=new Person();
68
}
69
public void FillData()
70
{
71
aPerson.Name="XML";
72
aPerson.Sex="男";
73
aPerson.Brith=DateTime.Now;
74
aPerson.SetID(1);
75
}
76
public void CreateXMLFile()
77
{
78
try
79
{
80
aFile=new FileStream("Person.xml",FileMode.Create);
81
}
82
catch(IOException e)
83
{
84
Console.WriteLine("some error happened!");
85
Console.WriteLine(e.ToString());
86
return;
87
}
88
aXML.Serialize(aFile,aPerson);
89
aFile.Close();
90
}
91
}
92
}
93
当然用Pascal也可以实现同样的功能,我们看看如何将上面的XML文件读取出来,这里只给出功能性代码,具体扩展希望读者自己思考。
代码(Pascal):
1
program AdapterClass;
2
//============== Program Description==============
3
//Name:AdapterClass.dpr
4
//Objective:AdapterClass
5
//Date:2006-05-04
6
//Written By coffee.liu
7
//================================================
8
{$APPTYPE CONSOLE}
9
10
uses
11
SysUtils,msxmldom,XMLDoc, oxmldom, xmldom, XMLIntf,windows;
12
type ISerTarget=interface
13
function ReadFile():string;
14
end;
15
type AdapteeXML=class
16
xmlDocument1 :IXMLDocument;
17
node:IXMLNode;
18
count:Integer;
19
constructor Create;
20
function ReadFromXml(Node:IXMLNode;Count:integer):string;
21
end;
22
type Adapter= class(AdapteeXML,ISerTarget)
23
private
24
FRefCount : Integer;
25
public
26
function QueryInterface(const IID:TGUID;out Obj):HRESULT;stdcall;
27
function _AddRef:Integer;stdcall;
28
function _Release:Integer;stdcall;//这里让接口引用计数器来控制对象生存周期了,这里我为了少写代码没有人为的去控制对象的生存周期,当然读者也可以通过继承TInterfacedObject对象来完成IInterface。 function ReadFile():string;
29
constructor Create;
30
end;
31
32
33
{ AdapteeXML }
34
35
constructor AdapteeXML.Create;
36
begin
37
xmlDocument1 :=TXMLDocument.Create(nil);
38
xmlDocument1.LoadFromFile('D:\coffee.liu\Delphi7\Projects\Person.xml');//上面C#例子创建出的XML文件
39
XMLDocument1.Active:=true;
40
count:=xmlDocument1.ChildNodes.Count;
41
node:= xmlDocument1.DocumentElement;
42
end;
43
44
function AdapteeXML.ReadFromXml(Node:IXMLNode;Count:integer):string;
45
var
46
i:integer;
47
begin
48
for i := 1 to Count-1 do begin
49
if(node<>nil) then
50
result:=Node.ChildNodes['Name'].Text+';'+Node.ChildNodes['Sex'].Text+';'+Node.ChildNodes['Brith'].Text;
51
52
end;
53
xmlDocument1.Active:=false;
54
end;
55
{ Adapter }
56
57
function Adapter._AddRef: Integer;
58
begin
59
Result := InterlockedIncrement(FRefCount);
60
end;
61
62
function Adapter._Release: Integer;
63
begin
64
Result := InterlockedDecrement(FRefCount);
65
if Result = 0 then
66
Destroy;
67
end;
68
69
function Adapter.QueryInterface(const IID: TGUID; out Obj): HRESULT;
70
begin
71
if GetInterface(IID, Obj) then
72
Result := 0
73
else
74
Result := E_NOINTERFACE;
75
end;
76
function Adapter.ReadFile():string;
77
begin
78
self.ReadFromXml(self.node,self.count);
79
end;
80
constructor Adapter.Create;
81
begin
82
inherited;
83
end;
84
var
85
aAdapter:Adapter;
86
begin
87
aAdapter:=Adapter.Create;
88
Writeln(aAdapter.ReadFile);
89
end.
90
对象的Adapter模式的结构:

从图中可以看出:客户端需要调用CreateFile方法,而AdapteeXML,AdapteeSoap,AdapteeBin没有该方法,为了使客户端能够使用AdapteeXXX类,需要提供一个包装(Wrapper)类Adapter。这个包装类包装AdapteeXXX的实例,从而将客户端与AdapteeXXX衔接起来。
由于Adapter与AdapteeXXX是委派关系,这决定了这个适配器模式是对象的。
该适配器模式所涉及的角色包括:
目标(Target)角色:这是客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口,这里为ISerTarget接口。
源(Adaptee)角色:需要适配的类,这里为AdapteeXML,AdapteeSoap,AdapteeBin类。
适配器(Adapter)角色:通过在内部包装(Wrap)Adaptee对象,把源接口转换成目标接口。
以上的例子主要为将Person类串行化为不同的形式的例子,这里分别保存为xml,soap,binary形式。由于我们使用了Adapter进行了统一封装,这样用户可以不必知道具体的封装细节,运用统一的CreateFile方法即可将Person类串行化为不同的形式。
具体代码(C#):
1
using System;
2
using System.IO;
3
using System.Xml.Serialization;
4
using System.Xml;
5
using System.Runtime.Serialization.Formatters.Binary;
6
/**//////运行时要将System.Runtime.Serialization.Formatters.Soap.dll的引用添加到项目中 7
using System.Runtime.Serialization.Formatters.Soap;
8
9
namespace AdapterObject
10

{
11
/**//// <summary>
12
///============== Program Description==============
13
///Name:AdapterObject.cs
14
///Objective:AdapterObject
15
///Date:2006-05-04
16
///Written By coffee.liu
17
///================================================
18
/// </summary>
19
class Class1
20
{
21
[STAThread]
22
static void Main(string[] args)
23
{
24
ISerTarget target=new Adapter("XML");
25
target.CreateFile();
26
Console.WriteLine("XML file created");
27
28
target=new Adapter("Soap");
29
target.CreateFile();
30
Console.WriteLine("Soap file created");
31
32
target=new Adapter("Bin");
33
target.CreateFile();
34
Console.WriteLine("Bin file created");
35
}
36
}
37
[Serializable]
38
public class Person
{
39
public string Name;
40
public string Sex;
41
public DateTime Brith;
42
private int PersonID;
43
public Person()
44
{}
45
public void SetID(int ID)
46
{PersonID=ID;}
47
}
48
49
interface ISerTarget
{
50
void CreateFile();
51
}
52
class Adapter:ISerTarget
{
53
private AdapteeXML aXML;
54
private AdapteeSoap aSoap;
55
private AdapteeBin aBin;
56
private string S;
57
public Adapter(string s)
{
58
S=s;
59
if (s=="XML")
60
{
61
aXML=new AdapteeXML();
62
aXML.FillData();
63
}
64
else
65
if (s=="Soap")
66
{
67
aSoap=new AdapteeSoap();
68
aSoap.FillData();
69
}
70
else if (s=="Bin")
71
{
72
aBin=new AdapteeBin();
73
aBin.FillData();
74
}
75
else
76
{
77
aXML=new AdapteeXML();
78
aXML.FillData();
79
}
80
}
81
82
public void CreateFile()
{
83
if (S=="XML")
84
{
85
aXML.CreateXMLFile();
86
}
87
else
88
if (S=="Soap")
89
{
90
aSoap.CreateSoapFile();
91
}
92
else if (S=="Bin")
93
{
94
aBin.CreateBinFile();
95
}
96
else
97
{
98
aXML.CreateXMLFile();
99
}
100
101
}
102
103
}
104
class AdapteeSoap
{
105
FileStream aFileSoap;
106
Person aPerson;
107
SoapFormatter aSoapFormatter;
108
public AdapteeSoap()
{
109
aSoapFormatter=new SoapFormatter();
110
aPerson=new Person();
111
}
112
public void FillData()
113
{
114
aPerson.Name="Soap";
115
aPerson.Sex="男";
116
aPerson.Brith=DateTime.Now;
117
aPerson.SetID(2);
118
}
119
public void CreateSoapFile()
{
120
try
121
{
122
aFileSoap=new FileStream("SoapPerson.xml",FileMode.OpenOrCreate);
123
aSoapFormatter.Serialize(aFileSoap,aPerson);
124
aFileSoap.Close();
125
}
126
catch(IOException e)
{
127
Console.WriteLine("some error happened");
128
Console.WriteLine(e.ToString());
129
return;
130
}
131
132
}
133
}
134
class AdapteeBin
135
{
136
FileStream aFileBin;
137
Person aPerson;
138
BinaryFormatter aBinaryFormatter;
139
public AdapteeBin()
140
{
141
aBinaryFormatter=new BinaryFormatter();
142
aPerson=new Person();
143
}
144
public void FillData()
145
{
146
aPerson.Name="Bin";
147
aPerson.Sex="男";
148
aPerson.Brith=DateTime.Now;
149
aPerson.SetID(3);
150
}
151
public void CreateBinFile()
152
{
153
try
154
{
155
aFileBin=new FileStream("BinPerson.data",FileMode.OpenOrCreate);
156
aBinaryFormatter.Serialize(aFileBin,aPerson);
157
aFileBin.Close();
158
}
159
catch(IOException e)
160
{
161
Console.WriteLine("some error happened");
162
Console.WriteLine(e.ToString());
163
return;
164
}
165
166
}
167
}
168
class AdapteeXML
{
169
FileStream aFile;
170
Person aPerson;
171
XmlSerializer aXML;
172
public AdapteeXML()
{
173
aXML=new XmlSerializer(typeof(Person));
174
aPerson=new Person();
175
}
176
public void FillData()
{
177
aPerson.Name="XML";
178
aPerson.Sex="男";
179
aPerson.Brith=DateTime.Now;
180
aPerson.SetID(1);
181
}
182
public void CreateXMLFile()
{
183
try
184
{
185
aFile=new FileStream("Person.xml",FileMode.Create);
186
}
187
catch(IOException e)
188
{
189
Console.WriteLine("some error happened!");
190
Console.WriteLine(e.ToString());
191
return;
192
}
193
aXML.Serialize(aFile,aPerson);
194
aFile.Close();
195
}
196
}
197
}
198
类适配器和对象适配器之间的差别:
1.当我们想匹配一个类和它所有子类时,类适配器将不能胜任,因为在创建子类时就已定义了派生它的基类。如上第二例,要想改为类适配器模式就必须使用三个Adapter来分别封装三个Adaptee,这样做不太实际。
2.类适配器允许适配器更改某些被匹配的类的方法,同时还允许使用其他未修改的方法。
3. 对象适配器通过将子类传递给构造函数而允许匹配所有子类,如上面的第二例。
4.对象适配器要求读者将希望使用的,被匹配对象的方法提到表面上来。