Strategy VS Mixin(番外篇)——动态混入扩展方法

    我那时什么也不懂!我应该根据她的行为,而不是根据她的话来判断她。她使我的生活芬芳多彩,我真不该离开她跑出来。我本应该猜出在她那令人爱怜的花招后面所隐藏的温情。花是多么自相矛盾!我当时太年青,还不懂得爱她。
                                                                                     ——圣埃克絮佩里
                                                                                           摘自《小王子》

摘要

如果扩展方法(Extension Method)可以动态选择要扩展的对象……

   *   *   *   *   *   *

这里有个非常有趣的设计。Quackable和Squeakable接口拥有相同函数签名的抽象函数WhoAmI()和Quack()。当Marllard Duck(以及Redhead Duck)同时实现了Quackable和Squeakable接口时,就被同时混入了两个版本的Quack()方法。这样就可以在运行期动态决定是使用Quackable.Quack()还是Squeakable.Quack()了。


有什么感觉?是不是很乱很强大?

感觉很强大,是因为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(), 就具有了吱吱叫的能力。


源代码下载: 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..
如果一只鸟外表像鸭子,游起泳来像鸭子并且叫起来像鸭子,那么它可能就是鸭子。

在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  =   ''            #  空字符串
=  [ 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#改成动态语言。

另一种方法是定义一个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的代理。例如
string  s  =   " abc " ;
HasLength s1 
=  DuckTyping.Implement < HasLength > (s);
将动态创建一个代理类Duck0
public   class  Duck0 : HasLength 
{
    
private   string  _obj;

    
public  Duck0( string  obj) 
    {
        
this ._obj  =  obj;
    }

    
int  HasLength.Length 
    {
        
get  
        {
            
return   this ._obj.Length;
        }
    }
}

然后就可以这样使用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
}

是的,要知道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://www.cnblogs.com/1-2-3/archive/2008/03/04/strategy-vs-mixin-ducktyping.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值