策略模式的应用实践

我的工作需要写一个话单转换工具,在写这个工具的过程中,发现整个实现恰恰可以说是策略模式最好的体现。也许用这个例子来说明策略模式的应用,最是合适。该话单转换工具的目的,是将某个服务提供商的话单文本文件,转换为另一个服务提供商的话单文本文件。如将联通的话单格式转换为移动的话单格式。而话单转换工具的要求,是希望能实现多个服务提供商话单文本文件的互相转换。

我们来分析一下任务。首先,各种服务提供商的话单格式,无疑是不相同的。例如,话单采集后,字段的顺序,字段的宽度以及字段间的分隔符,都不相同。但是,从总体上来说,话单的表现形式是大致相同的,这为我们实现话单转换提供了一个技术上可行的前提。

所谓话单转换,就是需要将一个话单文本文件读出,然后对每一行的字符串进行识别后,再转换为符合相对应的服务提供商标准的话单文本文件。操作很简单,就是文本文件的读写而已,不同的就在于转换的方法。根据服务提供商标准的不同,我们应该为每一种转换提供相对应的算法。而所谓策略模式,正是对算法的包装和抽象,将算法的责任和其本身分离。所以,我们现在要做的工作就是将转换话单的算法抽象出来。

根据工具的要求,话单转换应该包括3个方法:
1、将文件读出的一行字符串转换为对应的话单对象;
2、将一种话单对象转换为另一种话单对象;
3、将话单对象转换为字符串,以方便写入话单文本文件;

根据以上的分析,我们还需要为不同的话单格式建立相应的对象。例如:网通、联通和移动的话单格式对象分别为:

public class CNCCdr
{
    
//网通话单格式对象的公共属性;
}

public class CUCCdr
{
    
//联通话单格式对象的公共属性;
}

public class CMCdr
{
    
//移动话单格式对象的公共属性;
}

接下来就应该实现话单转换的算法了。首先需要将算法进行抽象,而进行抽象的最佳选择莫过于使用接口,例如我们定义一个用于话单转换的接口ICdrConvert:
public interface ICdrConvert
{
}
按照如前的分析,在接口中应该包括三个转换方法。但是现在有个问题,就是转换的话单对象。由于方法在接口中,为一个抽象。而话单对象可能有多种,具体转换为何种话单对象,需要到具体实现时才能决定。因此,在接口方法中,无论返回类型,还是传入参数,涉及到的话单对象只能是抽象的。也许我们可以考虑System.Object来表示话单对象,但更好的办法是为所有的话单对象也提供一个抽象接口。

由于从目前的分析来看,抽象话单对象并没有一个公共的方法,所以这个抽象的话单接口,是一个标识接口:
public interface ICdrRecord
{
}

现在,可以对转换接口的方法进行定义了:

public interface ICdrConvert
{
    ICdrRecord Convert(
string record);
    ICdrRecord Convert(ICdrRecord record);
    
string Convert(ICdrRecord record);
}

自然,这样的接口定义无法通过编译。为什么呢?是因为第二个方法的签名与第三个方法的签名重复了(方法的签名和返回类型无关)。因此,我们需要为第三个方法修改名字。

但我们仔细想想,第三个方法的转换在转换接口中是必要的吗?该方法的任务是将一个话单对象转换为string类型。实际上这个责任,并不需要专门的转换对象来完成,而应属于话单对象本身的责任。再想想.Net中,所有对象均派生于System.Object,而object类型均提供了ToString()方法。

从设计的角度来看,最好的办法,是在具体的话单对象中override System.Object的ToString()方法,而不是在转换对象中,提供该转换算法。

不过考虑到,在话单处理中,更多地会调用抽象话单接口类型的对象,也许我们将ToString()方法抽象到接口ICdrRecord中会更好。

public interface ICdrRecord
{
    
string ToString();
}

public interface ICdrConvert
{
    ICdrRecord Convert(
string record);
    ICdrRecord Convert(ICdrRecord record);
}


而具体的话单对象,就应该实现ICdrRecord接口了。因为各话单对象均继承了System.Object,则间接地继承了object对象的ToString()方法,所以,话单对象应该重写该方法:

public class CNCCdr: ICdrRecord
{
    
//网通话单格式对象的公共属性;

    
//重写object的ToString()方法,同时也实现了接口ICdrRecord的ToString()方法;
    public override string ToString()
    
{
        
//实现具体的内容;
    }

}

public class CUCCdr: ICdrRecord
{
    
//联通话单格式对象的公共属性;

    
//重写object的ToString()方法,同时也实现了接口ICdrRecord的ToString()方法;
    public override string ToString()
    
{
        
//实现具体的内容;
    }

}

public class CMCdr: ICdrRecord
{
    
//移动话单格式对象的公共属性;

    
//重写object的ToString()方法,同时也实现了接口ICdrRecord的ToString()方法;
    public override string ToString()
    
{
        
//实现具体的内容;
    }

}


下面就是关键的实现了。由于我们已经为转换算法进行了抽象,因此根据策略模式来实现具体的转换算法,就是水到渠成的事情了。实现代码之前,先来看看UML类图:
 tragedy.gif

注意看橙色部分,这一部分即为策略模式的主体。接口ICdrConvert为抽象策略角色,类CNCToCUC,CUCToCM为具体策略角色,它们分别实现了将网通话单转换为联通话单,联通话单转换为中国移动话单的算法。根据实际需要,还可以添加多个类似的具体策略角色,并实现ICdrConvert接口:

public class CNCToCUC:ICdrConvert
{
    
public ICdrRecord Convert(string record)
    
{
        
//实现具体的转换算法;
    }

    
public ICdrRecord Convert(ICdrRecord record)
    
{
        
//实现具体的转换算法;
    }

}

类CUCToCM的实现相似,不再重复。

那么通过策略模式实现,究竟有什么好处呢?请大家注意上图的CdrOp类。该类是抽象类,它提供了一个构造函数,可以传递ICdrConvert对象:

public abstract class CdrOp
{
    
protected ICdrConvert _convert;
    
protected string _sourceFileName;
    
protected string _targetFileName;
    
    
public CdrOp(string soureFileName,string targetFileName,ICdrConvert convert)
    
{
        _sourceFileName 
= soureFileName;
        _targetFileName 
= targetFileName;
        _convert 
= convert;
    }


    
public abstract void HandleCdr();    
}


类CdrFileOp继承了抽象类CdrOp:

public class CdrFileOp
{
    
public override void HandleCdr()        
    
{
        Read();
        Write();
    }

    
    
private string Read()
    
{
        
using (StreamReader sd = new StreamReader(_sourceFileName))
        
{    
            ICdrRecord cdr 
= null;    
            
string line;
            
while ((line = sd.ReadLine()) != null)
            
{
                
//首先调用ICdrRecord Convert(string record)方法;
                
//再调用ICdrRecord Convert(ICdrRecord record)方法;
                
//至于实现的是何种转换,由构造函数传入的ICdrConvert对象决定;
                cdr = _convert.Convert(_convert.Convert(line));
                _list.Add(cdr);                        
            }

        }

    }


    
private void Write()
    
{
        
using (StreamWriter sw = new StreamWriter(_targetFileName,true))
        
{                
            sw.Write(head.ToString());
            
foreach (ICdrRecord record in _list)
            
{
                sw.Write(record.ToString());
            }

        }

    }


    
private ArrayList _list = new ArrayList();
}


这个类,实现了抽象类CdrOp的HandleCdr()方法。但具体的实现细节则是在私有方法Read()和Write()中完成的(根据实际情况,也可以把Read和Write方法作为公共抽象方法或保护方法,放到抽象类CdrOp中,而在抽象类CdrOp中具体提供HandleCdr方法的实现,该方法调用Read和Write方法,这样就使用了模版方法模式)。

注意看Read和Write方法中,没有一个具体类。不管是话单对象,还是话单转换对象,均是抽象接口对象。尤其是在Read()方法中,调用了_Convert的Convert方法:
cdr = _convert.Convert(_convert.Convert(line));

其中,内部的Convert方法,即_convert.Convert(line),是将读出来的字符串转换为ICdrRecord对象,然后通过调用_convert.Convert(ICdrRecord record)方法,再将该对象转换为另一种话单格式对象,但类型仍然属于ICdrRecord。

那么在这些转换过程中,究竟转换成了哪一种话单格式对象呢?这是由_convert字段来决定的。而这个对象则是由构造函数的参数中传递进来的。

同样的道理,在Write()方法中,大家也可以看到为所有话单对象抽象为一个接口ICdrRecord的好处。通过ICdrRecord调用ToString()方法,避免了在CdrFileOp中引入具体对象。要知道,程序一旦引入具体对象,则耦合性就高了。一旦需求发生改变,就需要对编码进行修改。

有了以上的架构,客户端调用就非常方便了:

public class Client
{
    
public static void Main()
    
{
        
string sourceFileName = @"c:\CNCCdr.txt";
        
string target1= @"c:\CUCCdr.txt";
        
string target2= @"c:\CMCdr.txt";
        
        
//将网通话单转换为联通话单;
        ICdrOp op1 = new CdrFileOp(sourceFileName,target1,new CNCToCUC());
        op1.HandleCdr();

        
//将刚才转换生成的联通话单转换为移动话单;
        ICdrOp op2 = new CdrFileOp(target1,target2,new CUCToCM());
        op2.HandleCdr();        
    }

}


当然,我们还可以引入工厂模式来创建CdrFileOp对象。或者将ICdrConvert对象设置为CdrFileOp的公共属性,而非通过构造函数来传入。

通过本文的讲解,你会发现策略模式并没有什么神秘的,无非还是一种对象的抽象,唯一不同的是,策略模式是对算法的抽象而已。所以,“抽象才是硬道理”,当我们在作面向对象设计时,需时时刻刻记住这句话啊!

posted on 2005-02-23 19:45 张逸 阅读(4947) 评论(18)  编辑 收藏 网摘 所属分类: Design & Pattern

评论

#1楼 2005-02-24 00:18 CsOver

好文章!看了一遍,有点难.先收着!   回复  引用    

#2楼 2005-02-24 11:20 samuel      

学而不思则罔,思而不学则殆.
Nice!
  回复  引用  查看    

#3楼 2005-02-26 21:45 吕震宇      

好久没有看到你写的文章了:)不错!解决了关键的转换问题。不过我还是想谈谈我的想法:

1)目前只有联通、网通、移动三家公司,所以转换策略仅有两个。一旦公司数增加到五家或更多,策略就会急剧增长。是否可以考虑设立一个内部“格式”,其它格式都向这个格式看齐,以控制变化。

2)cdr = _convert.Convert(_convert.Convert(line));这条命令让我思考了半天。后来才反映过来此_convert.Convert与彼_convert.Convert不是一个Convert,应用了多态在里面。但并没有很好的体现“代码就是最好的注释”,因为这两个Convert的含义孑然不同。后来我才注意到,策略的命名是CNCToCUC,隐含的规定了数据的来源与数据的去处。所以_convert.Convert(line)是将源转换为“公共格式”(我是这么理解的),而后一个_convert.Convert是将公共格式转换为目标格式,两个Convert虽然名字相同意义确完全不同。是否可以考虑隔离接口?

我想,是否可以设计多个Reader与Writer,然后通过依赖注入来实现动态组合。Reader的功能是将一个格式转换成“公共”格式,而Writer是将公共格式转换成目标格式。其实XML、Memory Stream、File等就有很多的Reader与Writer相互转换(更象装饰模式)。
  回复  引用  查看    

#4楼[楼主] 2005-02-28 13:12 wayfarer      

谢谢吕震宇!

第二个建议很好,我在分析对象责任的时候,看来出现了错误。确实应该将这两个不同的Convert方法分离开。不过方法public ICdrRecord Convert(string record),并非是将其转换为公共格式,而是将一个字符串转换为具体的话单对象。因此,这个方法应该放到具体的话单对象中。

你说的第一个建议固然是好,可惜从我要实现的业务来看,不太可能。因为网通、联通和移动的话单不仅格式不一样,某些字段也不相同。如网通话单中,有些字段在联通中根本就没有。因此,要建立公共格式,至少需要一个标准才行,相对来说,这是比较困难的。

不过,可以以这个例子做个假设,写出更完美的方案来。
  回复  引用  查看    

#5楼 2005-03-02 08:55 吕震宇      

呵呵,好!其实只有针对具体的问题,模式才可能发挥作用,毕竟设计模式的目的也为的是更好的解决问题,而不是花架子。一切从实际出发才行。

我对实际情况不太了解,所以才提出了“公共格式”,现在看来没有什么必要了。
  回复  引用  查看    

#6楼 2005-03-06 01:06 寒枫天伤

本期博客园期刊使用了您的这篇文章,如果有异议,请发邮件到William_Fire@126.com   回复  引用    

#7楼 2005-05-09 23:16 pppp

如果就这个例子的话,用Templet Method模式应该也可以吧,把整体算法都写在抽象基类里作为模版,而将热点Read();和Write();方法做成virtual的在具体的子类里override

不过聚合优先于继承,策略模式应该更加灵活些,至少策略类还可以重用
  回复  引用    

#8楼[楼主] 2005-05-10 08:59 wayfarer      

@pppp:
(根据实际情况,也可以把Read和Write方法作为公共抽象方法或保护方法,放到抽象类CdrOp中,而在抽象类CdrOp中具体提供HandleCdr方法的实现,该方法调用Read和Write方法,这样就使用了模版方法模式)。

这一点我已在文中说明。本文的关键不在于Read和Write方法,而是构造函数中传递的参数ICdrConvert对象,这才是策略模式的应用。
  回复  引用  查看    

#9楼 2005-08-11 16:30 一一[未注册用户]

现在总算是能看明白这种程度的文章了!

在过一段时间就能给出不同的意见和看法了!

高兴一小会儿!
  回复  引用    

#10楼 2005-12-30 11:38 milkcow[未注册用户]

看到中间才反应过来不是java,汗....
UML图中好像CdrOp和ICdrconvert那根线应该反过来把,还少了个箭头:)
  回复  引用    

#11楼 2006-06-12 18:21 txd_lf[未注册用户]

几个转换类实在是没有必要,如果需要转换的格式增多,想想吧,要实现得类太多了。确立一个中间格式虽然困难但一定可行。你说的“如网通话单中,有些字段在联通中根本就没有”,那你怎么转换他们呢~既然这个可以办到,那么就可以和中间格式互相转化,所以对于这篇文章模式虽然应用的好,但设计应该多多优化。
还有一个就是我实在看不出来CdrOp和CdrFileOp两个类到底怎么回事,还请指教
  回复  引用    

#12楼 2007-01-16 17:26 Kim[未注册用户]

CdrOp与CdrFileOp 是为了抽象数据源的读取吗?   回复  引用    

#13楼 2008-01-18 14:17 kevin_supersun[未注册用户]

CdrFileOp是CdrOp的功能实现,所以从现实意义上来看,CdrFileOp是一组功能的集合,为什么CdrOp不以接口的方式出现而是使用抽象类呢?   回复  引用    

#14楼[楼主] 2008-01-21 09:21 Bruce Zhang      

@kevin_supersun
如果不考虑单继承的约束,最佳实践是优先使用抽象类而不是接口。
  回复  引用  查看    

#15楼 2008-08-21 23:14 无恙      

我觉得这个接口
public interface ICdrConvert
{
ICdrRecord Convert(string record);
ICdrRecord Convert(ICdrRecord record);
}
里面的两个方法为什么不合并一下?
不觉得有太大的必要区分开来,直接一个ICdrRecord Convert(string record)就可以了,
这样也不会有后面这段_convert.Convert(_convert.Convert(line))让人觉得有点奇怪的代码。
  回复  引用  查看    

#16楼 2008-12-05 18:15 坷垃和克拉      

有点晦涩   回复  引用  查看    

#17楼 2009-03-23 11:10 重庆串串      

没看懂.....   回复  引用  查看    




发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 108237




相关文章:

相关链接:

导航

公告

我的个人主页:
logo.gif
我的著作与译作

《软件设计精要与模式》

《WCF服务编程》

MVP_Horizontal_BlueOnly.png

From 03-03-2006
Counter: site stats
审批小组成员时,如有超过6个待审批成员,无法翻页察看。操作不便。

与我联系

搜索

 

常用链接

我参加的小组

我参与的团队

随笔分类(266)

随笔档案(258)

最新随笔

积分与排名

最新评论

阅读排行榜

评论排行榜