——圣埃克絮佩里
摘自《小王子》
摘要
如果扩展方法(Extension Method)可以动态选择要扩展的对象……
* * * * * *
这里有个非常有趣的设计。Quackable和Squeakable接口拥有相同函数签名的抽象函数WhoAmI()和Quack()。当Marllard Duck(以及Redhead Duck)同时实现了Quackable和Squeakable接口时,就被同时混入了两个版本的Quack()方法。这样就可以在运行期动态决定是使用Quackable.Quack()还是Squeakable.Quack()了。
![](https://i-blog.csdnimg.cn/blog_migrate/4044353de751dac97208ecdfd493613b.png)
有什么感觉?是不是很乱很强大?
感觉很强大,是因为Mallard Duck就像变相怪杰,换个面具(interface),行为就完全不同了,而且是动态的哟。以前,这种“运行期动态改变对象行为”的功能只能用Strategy才能实现。虽然背后的运作原理不同,但这个设计仍然可以给人一种duck.Quack()方法的行为被“动态”改变了的感觉。
它可以作为Strategy的替代品么?不,决不能。Strategy表现出来的语义是“Mallard Duck有一种鸣叫的方法——嘎嘎叫 或吱吱叫”(Strategy的例子可以参考 上一篇)。再来看一下图一,Mallard Duck同时实现了Quackable和Squeakable接口,所表现出来的语义是“Mallard Duck既能嘎嘎叫 同时又能吱吱叫”,这可就和我们的本意相悖了。
Strategy模式是把对象的行为提炼成显式的概念,一大好处是你只要看一看有哪些类实现了QuackBehavior接口,就知道一共有多少种鸣叫的方法了。这个在图一的设计里是完全体现不出来的。
难道说Strategy就是Strategy,天上地下独一无二的Strategy?
难道说我们一不小心又弄了个垃圾设计?
在急着把它扔进垃圾箱之前,不妨再作一个设想:如果可以在运行期动态决定让Mallard Duck实现哪个接口又会如何?
源代码下载:GeekDesign.rar (VS2008控制台工程)
动态实现接口
这个设计与图一的设计只有一点点不同:Mallard Duck(以及Redhead Duck)在编译期既不实现Quackable接口也不实现Squeakable接口。这样,在一开始的时候(编译期)Mallard Duck没有鸣叫的能力,但是有鸣叫的潜力(因为实现了WhoAmI()函数)。在运行期,通过让Mallard Duck动态实现Quackable接口,使它被动态混入QuackModule.Quack()函数,就具有了嘎嘎叫的能力;或动态实现Quackable接口,使它被动态混入Squeakable.Quack(), 就具有了吱吱叫的能力。
![](https://i-blog.csdnimg.cn/blog_migrate/28a18b91635f75cb3042d90b0dc1a99b.png)
源代码下载: DuckTyping.rar(VS2008控制台工程)
怎么样?虽然只进行了一点点改变,感觉却完全不同了。当然,本例的“动态混入不同的算法”和Strategy的“动态替换不同的算法”还是有一些差别。但是不管怎么说,我们的设计工具箱里又多了件超灵活的利器。
但是它是怎么实现的?呵呵,是使用了CodeProject上的一个开源类库 NDuck。我知道很多人一听到开源项目或第三方的东东就会头痛了。不过请相信我,NDuck真的是超级小巧超级简便的——小巧到全部源代码才几百行,简便到无需任何配置只要引用一个NDuck.dll文件就可以使用了。
附录
1. Duck Typing 简介
Duck Typing是一种基于动态类型的编程方式。在这种方式里,将根据一个对象当前所具有的方法和属性集来决定有效语义,而不是根据这个对象所继承的类。(这个我喜欢。王侯将相,宁有种乎?) Duck Typing这名称来源于Duck Test。
Duck Test
If a bird looks like a duck, swims like a duck and quacks like a duck, then it's probably a duck..
如果一只鸟外表像鸭子,游起泳来像鸭子并且叫起来像鸭子,那么它可能就是鸭子。
If a bird looks like a duck, swims like a duck and quacks like a duck, then it's probably a duck..
如果一只鸟外表像鸭子,游起泳来像鸭子并且叫起来像鸭子,那么它可能就是鸭子。
在Duck Typing里,我们只关心对象是否实现了需要被使用的那些方面,而不是对象本身的类型。就像我喜欢张曼玉这种类型的,但却不是非张曼玉或张曼玉的女儿不娶。
在C#这样的静态类型语言里,我们可能会定义这样一个用于判断字符串是否为空的函数IsEmpty().
bool IsEmpty(String arg)
{
return arg.Length <= 0;
}
虽然数组或Stream对象都有Length属性,却不能使用这个函数。要想判断它们是否为空,我们必须再写两个IsEmpty()函数。
bool IsEmpty(Array arg)
{
return arg.Length <= 0;
}
bool IsEmpty(Stream arg)
{
return arg.Length <= 0;
}
而在Ruby这种动态类型的语言里,只需定义一个IsEmpty()函数就行了。
#
判断arg是否为空
def is_empty?(arg)
return arg.length <= 0
end
s1 = ' abc ' # 字符串
s2 = '' # 空字符串
a = [ 1 , 3 , 5 ] # 数组
puts is_empty?(s1) # => false
puts is_empty?(s2) # => true
puts is_empty?(a) # => false
def is_empty?(arg)
return arg.length <= 0
end
s1 = ' abc ' # 字符串
s2 = '' # 空字符串
a = [ 1 , 3 , 5 ] # 数组
puts is_empty?(s1) # => false
puts is_empty?(s2) # => true
puts is_empty?(a) # => false
2. NDuck 项目简介
不论你是否喜欢Duck Typing,应该都会同意在上面那个C#的例子中写3个几乎一模一样的IsEmpty()函数很是累赘。有什么方法可以只写一个函数么?
C#提供编译期的类型检查,但是也提供了绕过类型检查的方法——反射。我们可以让IsEmpty()函数接收Object类型的参数,然后使用反射获得参数的Length属性。
static
bool
IsEmpty(Object arg)
{
// 通过反射取得arg的Length属性的值
int length = ( int )arg.GetType().GetProperty( " Length " ).GetValue(arg, null );
return length <= 0 ;
}
这个IsEmpty()函数可以适用于任何拥有Length属性的对象。但是千万别把它用于你的程序,它会让程序慢得像蜗牛。而且使用反射又麻烦又不直观。我宁肯去微软总部示威游行,要求把C#改成动态语言。
{
// 通过反射取得arg的Length属性的值
int length = ( int )arg.GetType().GetProperty( " Length " ).GetValue(arg, null );
return length <= 0 ;
}
另一种方法是定义一个HasLength接口,让IsEmpty()针对这个接口编程。
public
interface
HasLength
{
int Length { get ; }
}
static bool IsEmpty(HasLength arg)
{
return arg.Length <= 0 ;
}
这个版本的IsEmpty()函数适用于任何实现了HasLength接口的对象。但是我们没法修改String、Array和Stream的源代码,怎能让它们实现HasLength接口呢?还记得那句经典的“Any problem in computer science can be solved with another layer of indirection.”么?NDuck实现动态实现接口的方法就是动态创建一个代理类Duck0,让这个代理类实现HasLength接口并作为String的代理。例如
{
int Length { get ; }
}
static bool IsEmpty(HasLength arg)
{
return arg.Length <= 0 ;
}
string
s
=
"
abc
"
;
HasLength s1 = DuckTyping.Implement < HasLength > (s);
将动态创建一个代理类Duck0
HasLength s1 = DuckTyping.Implement < HasLength > (s);
public
class
Duck0 : HasLength
{
private string _obj;
public Duck0( string obj)
{
this ._obj = obj;
}
int HasLength.Length
{
get
{
return this ._obj.Length;
}
}
}
{
private string _obj;
public Duck0( string obj)
{
this ._obj = obj;
}
int HasLength.Length
{
get
{
return this ._obj.Length;
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/d137be5a0e651d4230306133390a460d.png)
然后就可以这样使用IsEmpty()了。
static
void
Main(
string
[] args)
{
string s = " abc " ;
int [] a = new int [] { };
HasLength s1 = DuckTyping.Implement < HasLength > (s);
HasLength a1 = DuckTyping.Implement < HasLength > (a);
Console.WriteLine(IsEmpty(s1)); // 输出: False
Console.WriteLine(IsEmpty(a1)); // 输出: True
Console.WriteLine(s1.GetType().Name); // 输出: Duck0
}
{
string s = " abc " ;
int [] a = new int [] { };
HasLength s1 = DuckTyping.Implement < HasLength > (s);
HasLength a1 = DuckTyping.Implement < HasLength > (a);
Console.WriteLine(IsEmpty(s1)); // 输出: False
Console.WriteLine(IsEmpty(a1)); // 输出: True
Console.WriteLine(s1.GetType().Name); // 输出: Duck0
}
是的,要知道HasLength接口拥有哪些函数还是要使用反射,还有动态生成、编译代码,这些工作肯定会耗费一些时间。但是NDuck保证这些工作只会进行一次,然后这些动态生成的代理类就会被缓存起来以备后用。所以,如果你的程序是用于心脏起搏器或控制火箭发射,我不建议你使用它,其它情况则不必太担心。
参考文献
Thomas et al, 孙勇等 译, Programming Ruby 中文版。电子工业出版社,2007.
Russ Olsen, Design Patterns in Ruby. Addison-Wesley, 2007.
Guenter Prossliner, DuckTyping: Runtime Dynamic Interface Implementation. codeproject, 2006.
Duck typing. Wikipedia. (可能需要使用代理)
![](https://i-blog.csdnimg.cn/blog_migrate/2911c77e9bcc80097ea009e20b6b9c2e.png)