UML软件工程组织

.Net中的设计模式——Decorator模式

 

作者:Bruce Zhang's Blog   文章来源:Bruce Zhang's Blog

 

一、模式概述

一个场景是我们要为一个对象动态添加新的职责,这个职责并不修改原有的行为,而是在原有行为基础上添加新的功能,就好比装饰工人为一座新居的墙上涂抹上色彩缤纷的颜料一般。

 从我们拥有的面向对象的知识出发,为一个对象增加新的职责,完全可以利用继承机制,然而再通过实例化派生的子类,来获得新增的职责。由于需要在原有行为基础上添加新功能,此时父类的方法应该为虚方法,例如用户登录行为:

public class User
{
public virtual void SignIn()
{
Console.WriteLine("The User Sign In.");
}
}

如果需要为用户登录行为增加权限验证的职责,可以定义一个子类继承User类:

public class SecurityUser:User
{
public override void SignIn()
{
if (IsValid())
{
base.SignIn();
}
else
{
throw new NotAuthorizationException();
}
}
private bool IsValid()
{
//略;
return true;
}
}

实现的类图如下:



 然而继承机制的一个局限是它只能静态地添加对象职责,一旦添加的职责有变,例如客户需求为登录行为添加日志记录功能,虽然我们可以再定义一个子类,重写SignIn()方法,然而我们却不能控制职责添加的时机与方式。此外,当User具有本身的继承体系时,则其并不能重用SecurityUser的职责。例如,User同时具有另外一个子类Employee,则Employee类的登录行为仍然沿用了其父类User的登录行为,而不具备权限验证的职责。如果需求要求Employee同样要验证登录权限,就必须再为它创建一个子类SecurityEmployee,如图:


 这样的结果会导致类的数量会随着需求的变化而无限量的增加,同时有关权限验证的职责也没有能够得到充分的重用,这显然违背了面向对象设计的精神。

 还有一个添加职责的办法,就是利用组合,将原有的对象嵌入到一个新的类中,由此来完成对新职责的添加,例如:

public class SecurityUser
{
private User m_user;
public User User
{
get {return m_user;}
set {m_user = value;}
}
public void SignIn()
{
if (IsValid())
{
m_user.SignIn();
}
else
{
throw new NotAuthorizationException();
}

}
private bool IsValid()
{
//略;
return true;
}
}

实现的类图如下:


 比起继承机制而言,组合方式更加的灵活,尤其当面对User具有自身的继承体系时,如上的设计不需要任何修改,就能从容的应对:


 此时我们只需要将Employee对象赋给SecurityUser中的User属性就可以实现对Employee权限验证的登录。因此,我们避免了此前采用继承机制所面临的两个问题:

 1、 类的无限量增加(所谓的“类爆炸”);
 2、 权限验证职责的不可重用。

 然而组合却失去了继承的许多优势,其中,不具备对象的多态特质,就是最大的一个局限。例如,如下创建对象的方式就是错误的:

 User user = new SecurityUser();

 当一个方法体现为对User的操作时,此时SecurityUser就无法对User类型的对象进行替代,这就限制了软件的可扩展性。例如,在表示层逻辑中,会有一个登录页面将调用SignIn方法:

public class LoginPage
{
public static void SignIn(User user);
}

在这种情况下,SecurityUser对象就无法做为参数传入,那么,我们在前面所作的职责添加的努力岂不就是付诸东流了吗?

 如果仔细观察继承与组合的优劣,我们发现如果将继承机制与组合方式两者结合起来,将会起到意想不到的效果,此时,各自的优点弥补了各自的缺点,完美地解决了“为对象动态添加新的职责”的需求。

 新的解决方案如下图所示:


 实际上,如上的类图结构就是设计模式中的Decorator模式,User是被装饰的对象,而SecurityUser则是Decorator,也就是我们所谓的“装饰工”。在这里,SecurityUser是一个具体类,如果有新的装饰需求,例如之前提到的增加日志记录功能,同样需要建立装饰类。此时,对于具体的装饰类而言,具有一些相同的逻辑,在此前提下,可以为其增加一个Decorator抽象类:


 此时,我将原来的SecurityUser类更名为SecuritySignInDecorator,便于理解。SignInDecorator是一个抽象类,继承了User类,同时User类对象又作为一个属性存在于SignInDecorator抽象类中。注意,这里的组合方式其实有多种实现方式。如作为一个属性,或者作为构造函数的参数等等。

 抽象类SignInDecorator的代码如下所示:

public abstract SignInDecorator:User
{
private User m_user;
public User User
{
get {return m_user;}
set {m_user = value;}
}
public override void SignIn()
{
if (m_user != null)
{
m_user.SignIn();
}
}
}

而SignInDecorator的子类定义则如下:

public class SecuritySignInDecorator:SignInDecorator
{
public override void SignIn()
{
if (IsValid())
{
base.SignIn();
}
else
{
throw new NotAuthorizationException();
}
}
private bool IsValid()
{
//略;
return true;
}
}
public class LoggingSignInDecorator:SignInDecorator
{
public override void SignIn()
{
base.SignIn();
Logging();
}
private void Logging()
{
//略;
}
}

目前的结构完全解决了前面利用继承或组合所出现的问题,避免了类的无限增加,权限验证或者日志记录的职责也能很好地重用,同时权限验证和日志记录等装饰类由于同样继承了User,因此根据多态原理,是可以完全替换User类型的对象的。
此外,利用Decorator模式还可以解决动态组合装饰的问题,例如为SignIn()方法既添加权限验证功能,又添加日志记录功能,此时并不需要新增一个类。实现如下:

User user = new User();
SignInDecorator securtiyDec = new SecuritySignInDecorator();
securityDec.User = user;
SignInDecorator loggingDec = new LoggingSignInDecorator();
loggingDec.User = securityDec;
loggingDec.SignIn();

loggingDec.SignIn()方法执行的时序图如下:


 从时序图中可以看到,当我们调用LoggingSignInDecorator对象的SignIn()方法时,因为LoggingSignInDecorator对象的User属性值为SecuritySignInDecorator对象,所以将执行SecuritySignInDecorator对象的SignIn()方法,该方法会先执行IsValid()私有方法,然而再调用User属性对象的SignIn()方法。由于SecuritySignInDecorator的User属性值为User对象,因此执行User对象的SignIn()方法。待SignIn()方法执行完毕,最后再执行LoggingSignInDecorator对象的Logging()方法。

 注意上述的时序图,如果在LoggingSignInDecorator对象的SignIn()方法中,Logging()方法放在base.SignIn()前,则应该先执行Logging()方法,然后才是SignIn()方法。

 不需要添加新的Decorator对象,通过上述的实现方式,我们就轻易地完成了对User对象SignIn()方法权限控制和日志记录的装饰。

二、.Net Framework中的Decorator模式

在.Net Framework中,有关流的处理就使用了Decorator模式。我们知道,所有的流操作都有一个共同的基类System.IO.Stream,它是一个抽象类,主要包含了Read、Write等行为。而针对文件流和网络流定义的类FileStream和NetworkStream,都继承了Stream类,它们的读写操作的实现自然是不同的。然而,当我们需要提高流读写性能的时候,不管是文件流还是网络流,.Net都提供了同样的方式,即通过Buffer存放流数据以达到性能改进的目的。此时,Buffer的作用对于流的读写操作而言,就相当于一个装饰的作用。同样的,如果我们要求对文件流或网络流的数据读写进行加密操作,以保障数据的安全,那么这个加密的职责同样是一种装饰作用,并且Buffer与加密职责是并行不悖的,有时候也许需要为流操作共同加上这两项职能,这些要求完全符合Decorator模式。

 在.Net Framework中,以上的实现可以用类图来表示:


 在类图中,BufferedStream和CryptoStream就相当于Decorator类,不过在这里并没有定义抽象的Decorator类,因为它们不需要有共同的逻辑进行抽象。

 .Net Framework对于Stream类的定义如下:

public abstract class Stream:MashalByRefObject,IDisposable
{
static Stream()
{
Stream.Null = new Stream.NullStream();
}
protected Stream()
{
this._asyncActiveCount = 1;
}
public abstract int Read([In,Out]byte[] buffer, int offset, int count);
public abstract void Write(byte[] buffer, int offset, int count);
//……
[NonSerialized]
private int _asyncActiveCount;
}
注意在BufferedStream和CryptoStream类中是在构造函数中传入要装饰的对象:
public sealed class BufferedStream:Stream
{
public BufferedStream(Stream stream):this(stream,0x1000){}
public BufferedStream(Stream stream, int bufferSize)
{
if (stream == null)
{
throw new ArgumentNullException("stream");
}
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException("bufferSize", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBePositive"), new object[] { "bufferSize" }));
}
this._s = stream;
this._bufferSize = bufferSize;
if (!this._s.CanRead && !this._s.CanWrite)
{
__Error.StreamIsClosed();
}
}
public override int Read([In, Out] byte[] array, int offset, int count)
{
//……
if (this._s == null)
{
__Error.StreamIsClosed();
}
int num1 = this._readLen - this._readPos;
if (num1 == 0)
{
//处理buffer的操作
num1 = this._s.Read(this._buffer, 0, this._bufferSize);
if (num1 == 0)
{
return 0;
}
this._readPos = 0;
this._readLen = num1;
}
//……
Buffer.InternalBlockCopy(this._buffer, this._readPos, array, offset, num1);
this._readPos += num1;
if (num1 < count)
{
int num2 = this._s.Read(array, offset + num1, count - num1);
num1 += num2;
this._readPos = 0;
this._readLen = 0;
}
return num1;
}
//Write与其它方法略
private Stream _s;
}

BufferedStream类中private字段_s为Stream类型,在Read()方法中,处理了Buffer的操作后,就将调用_s的Read()方法,以此实现对Stream对象Read行为的装饰功能。

 CryptoStream类的实现与BufferedStream类似,都是继承了抽象类Stream,同时通过构造函数的参数引入一个Stream对象。

 在使用BufferedStream和CryptoStream时,可以将FileStream或NetworkStream等Stream子类对象通过其构造函数传入,以达到装饰的目的。由于其本身也继承了Stream类,因此它们互相之间也可以装饰,例如下面的代码:

public static void EncryptTextToFile(String Data, String FileName, byte[] Key, byte[] IV)
{
try
{
FileStream fStream = File.Open(FileName, FileMode.OpenOrCreate);

// Create a new Rijndael object.
Rijndael RijndaelAlg = Rijndael.Create();

// Create a CryptoStream using the FileStream
// and the passed key and initialization vector (IV).
CryptoStream cStream = new CryptoStream(fStream,
RijndaelAlg.CreateEncryptor(Key, IV),
CryptoStreamMode.Write);
// Create a StreamWriter using the CryptoStream.
StreamWriter sWriter = new StreamWriter(cStream);
try
{
// Write the data to the stream to encrypt it.
sWriter.WriteLine(Data);
}
catch (Exception e)
{
Console.WriteLine("An error occurred: {0}", e.Message);
}
finally
{
sWriter.Close();
cStream.Close();
fStream.Close();
}
}
catch (CryptographicException e)
{
Console.WriteLine("A Cryptographic error occurred: {0}", e.Message);
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine("A file error occurred: {0}", e.Message);
}
}

在这里,就是将FileStream对象通过如下代码组合在CryptoStream对象中:

CryptoStream cStream = new CryptoStream(fStream,
RijndaelAlg.CreateEncryptor(Key, IV),
CryptoStreamMode.Write);

然后StreamWriter再利用该CryptoStream对象进行写操作时,写到文件FileName中的,就是加密后的数据。也就是说,我们动态地为FileStream添加了加密的职能。

 


版权所有:UML软件工程组织