策略模式的应用实践

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

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

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

根据工具的要求,话单转换应该包括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 @ 2005-02-23 19:45  张逸  阅读(11380)  评论(24编辑  收藏  举报