应用OOP的设计过程演化(三)
 

2009-11-16 作者:beniao 来源:beniao的BLOG

 

 在上一篇文章里(应用OOP的设计过程演化(二))完善了整个系统的体系结构,以及完成了各个具体的功能角色的功能,这也只能算是完成了一个结构而已,要真正做到完善还差得很远。比如在计算租金这个算法上,使用switch语句,判断图书的类型来决定该书的折扣,之前我为了演示在switch语句中固定了折扣的算法策略,如下代码示意代码:

 1/// <summary>
 2/// 计算租金
 3/// </summary>
 4/// <returns></returns>

 5private double GetRent()
 6{
 7    switch (_bType)
 8    {
 9        case B_Type.NOVEL: BookCash = Convert.ToDouble(Day) * 0.1;
10            break;
11        case B_Type.LIFT: BookCash = Convert.ToDouble(Day) * 1d;
12            break;
13        case B_Type.MAGAZINE: BookCash = Convert.ToDouble(Day) * 0.5;
14            break;
15    }

16    return BookCash;
17}

这就是原有设计中的普通顾客借书的折扣算法, 租借的是小说,租金的折扣*0.1,生活类书籍租金的折扣*1,而杂志则是打5折,很显然这样的固化设计是不合理的,那我们应该怎么来解决呢?,在实际的应用开发中,我们应该从数据库或是配置文件里读取这些折扣率,下面我以从配置文件中读取的方式简单介绍下。

我们可以这样来分析,因为不同类型的书籍的折扣是不一样的,在系统中又出现两种角色(会员和普通顾客),那么会员的折扣率和普通顾客的折扣率也应该有所不同,我们应该为他们定义不同的算法策略,将每一种折扣算法封装到一个类中,然后我们只需要根据书籍的类型里进行判断决定采用哪一个类(具体的策略对象)来处理就OK。通过这样的分析,策略模式(Strategy)正是解决这样的问题的模式,它的定义:"准备一组算法,并将每一个算法封装起来,使得它们可以互换。"

策略模式的使用是由用户发起的,根据用户的操作决定使用什么具体的策略角色。也就是说,我们完全可以使用这个模式来解决上面这种固化的折扣算法。系统里书籍的类型分三种:小说、生活和杂志;我们可以为这三种类型的书籍各自定义一个独立的算法策略,当然也可以将这三种策略定义到一个类里。我们既然是面向对象的编程,那就还是将其分开吧。但要记住的是“面向对象编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。” 在实际的项目中如果能够做到这样也就够了,类的划分还得根据实际的需求而定。

策略模式中分有三种参与者角色:

环境角色:持有一个Strategy类的引用。

抽象策略角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

具体策略角色:包装了相关的算法或行为。

用一句话概括:“策略模式就是一个提倡面向接口编程的模式”。在完成上面所提出的租金计算策略之前,我们来看一个策略模式的简单示例。比如我们有这样一个需求,当我们的系统在记录操作日志的时候,客户要求提供多种日志记录策略,通过设置可以将日志记录到不同的地方(文本文件、XML文件以及数据库等),那么根据策略模式的定义,我们完全可以把不同的日志记录算法定义为一个独立的策略对象。

1/// <summary>
2/// 抽象策略角色:这是一个抽象角色,通常由一个接口或抽象类实现。
3/// 此角色给出所有的具体策略类所需的接口。
4/// </summary>

5public abstract class Strategy
6{
7    public abstract void WriteLog();
8}

 1/// <summary>
 2/// 具体策略角色:包装了相关的算法或行为。
 3/// </summary>

 4public class TXTStrategy:Strategy
 5{
 6    public override void WriteLog()
 7    {
 8        Console.WriteLine("日志被记录到TXT文件!");
 9    }

10}

 1/// <summary>
 2/// 具体策略角色:包装了相关的算法或行为。
 3/// </summary>

 4public class XMLStrategy:Strategy
 5{
 6    public override void WriteLog()
 7    {
 8        Console.WriteLine("日志被记录到XML文件!");
 9    }

10}

 1/// <summary>
 2/// 具体策略角色:包装了相关的算法或行为。
 3/// </summary>

 4public class DBStrategy:Strategy
 5{
 6    public override void WriteLog()
 7    {
 8        Console.WriteLine("日志被记录到数据库!");
 9    }

10}

 1/// <summary>
 2/// 环境角色:持有一个Strategy类的引用
 3/// </summary>

 4public class Context
 5{
 6    Strategy strategy;
 7    /// <summary>
 8    /// 初始化时传入具体的策略对象
 9    /// </summary>
10    /// <param name="strategy"></param>

11    public Context(Strategy strategy)
12    {
13        this.strategy = strategy;
14    }

15
16    public void ContextStrategy()
17    {
18        strategy.WriteLog();
19    }

20
21    /// <summary>
22    /// 动态设置策略
23    /// </summary>
24    /// <param name="strategy"></param>

25    public void SetStrategy(Strategy strategy)
26    {
27        this.strategy = strategy;
28    }

29}

定义了一个抽象策略角色(Strategy),三种不同的日志记录策略(TXTStrategy、XMLStrategy和DBStrategy),也就是模式参与者中的具体策略角色,在环境角色里持有一抽象策略角色的引用,并通过构造器传入具体的策略对象初始化策略角色的引用,我们还定义了一动态设置策略的方法(SetStrategy),那么客户端就可以这样来使用这个策略:

 1namespace DesignPattern.Strategy
 2{
 3    class Program
 4    {
 5        static void Main(string[] args)
 6        {
 7            //初始化时传入具体的策略对象
 8            Context context = new Context(new XMLStrategy());
 9            context.ContextStrategy();
10
11            //设置策略对象
12            context.SetStrategy(new DBStrategy());
13            context.ContextStrategy();
14
15            context.SetStrategy(new TXTStrategy());
16            context.ContextStrategy();
17        }

18    }

19}

UML图如下:

好像说了很多的费话,那下面我们还是回到主题,看看本文案例中的租金折扣计算策略应用策略模式的实现。

根据我们上面的分析可知,策略模式是一个提倡针对接口变成的模式,而使用接口的目的是为了统一标准或着说是制定一种强行的规定,不同类型书籍的折扣率的不同但在程序中是计算算法是相同的,计算最终的价格=定价*折扣率(这还与用户类型相关)。那既然算法都相同我们就有必要为其指定一个标准(抽象出一个接口或是抽象类):

 1namespace EBook.Library.Strategy
 2{
 3    public interface IBookStrategy
 4    {
 5        /// <summary>
 6        /// 不同的书籍类型有不同的折扣策略
 7        /// </summary>
 8        /// <returns></returns>

 9        double GetRate();
10    }

11}

接口已经定义好了,那不同的折扣计算就只需要实现这个接口就OK了,就如上面的需求来说,只需要让不同的书籍的具体的折扣算法来继承这个接口去实现他们各自的计算就OK。

按照常理分析,具体的折扣率是应该存放在数据库或是配置文件中的(这里的配置文件不只局限于web.config或是App.config,普通的文本文件或是XML都可以),在这里我为了清楚的演示就将数据配置到应用程序配置文件(控制台下调试)里以方便读取,上面已经分析得很清楚了,怎么配置就不多说,具体配置如下:

 1<?xml version="1.0" encoding="utf-8" ?>
 2<configuration>
 3  <appSettings>
 4    <!--会员购书折扣-->
 5    <add key="NovelStrategy" value="0.7"/>
 6    <add key="LiftStrategy" value="0.8"/>
 7    <add key="MagezineStrategy" value="0.6"/>
 8
 9    <!--普通顾客购书折扣-->
10    <add key="SNovelStrategy" value="0.9"/>
11    <add key="SLiftStrategy" value="1"/>
12    <add key="SMagezineStrategy" value="0.8"/>
13
14    <!--书籍类型折扣策略-->
15    <add key="NOVEL" value="EBook.Library.Strategy.RateStrategy.NovelStrategy"/>
16    <add key="LIFT" value="EBook.Library.Strategy.RateStrategy.LiftStrategy"/>
17    <add key="MAGAZINE" value="EBook.Library.Strategy.RateStrategy.MagezineStrategy"/>
18
19    <add key="assembly" value="EBook.Library"/>
20  </appSettings>
21</configuration>

上面把策略的类配置到了配置文件里,这是为了方便后面通过反射来创建对的实例使用。由于有三个分类,这里我就以生活类(LIFT)书籍作为示例,介绍下怎么去实现,其他的两类实现基本相同,由于系统里出现了两种角色(会员和普通顾客),而两种类型的拥护在折扣的计算上也是不同的,从配置文件可以看出,会员购买生活类书籍的折扣是0.8,而普通顾客则是1(不打折)。这能说明什么?我们在具体的折扣计算策略里需要加入对用户类型的判断,是这样的吗?

 1namespace EBook.Library.Strategy.RateStrategy
 2{
 3    /// <summary>
 4    /// 生活类书的折扣策略
 5    /// </summary>

 6    public class LiftStrategy:IBookStrategy
 7    {
 8        public LiftStrategy() { }
 9
10        private U_Type uType;
11        public LiftStrategy(U_Type uType) 
12        {
13            this.uType = uType;
14        }

15
16        public double GetRate()
17        {
18            string lift = string.Empty;
19            if (uType.ToString() == U_Type.MEMBER.ToString())
20            {
21                lift = ConfigurationManager.AppSettings["LiftStrategy"];
22            }

23            else
24            {
25                lift = ConfigurationManager.AppSettings["SLiftStrategy"];
26            }

27            return double.Parse(lift);
28        }

29    }

30}

通过对用户类型的判断决定返回具体的折扣率。到这里,我们得回到具体的业务对象里去修改原有的TGetMoney方法了。由于具体业务对象有五个,这里我以会员购书为例(Buy)在分析下。根据上面的配置,TGetMoney方法就可以直接读取配置文件然后通过反射来创建具体的策略对象,并调用其获取折扣率的方法GetRate():

1public override double TGetMoney()
2{
3    IBookStrategy book = null;
4    string strategy = ConfigurationManager.AppSettings[_bType.ToString()];
5    string assembly = ConfigurationManager.AppSettings["assembly"];
6    book = (IBookStrategy)Assembly.Load(assembly).CreateInstance(strategy);
7    BookCash = BookPrice * book.GetRate();
8    return BookCash;     //此代码实现有"问题"详细见下面回复第8楼
9}

同前面一样,我们可以先来做个简单的测试,看这样的修改相比前面的设计是不是达到同样的效果。

1IMoney m = new Buy(B_Type.LIFT, "beniao""ASP.NET"53.50);
2Console.WriteLine(m.GetMoney());
3Console.WriteLine(m.Execute());
运行结果如下: 42.8
尊敬的会员:beniao,您购买《ASP.NET》,定价为:53.5元,折扣后为:42.8元

测试结果告诉了我们,上面的修改可以和以往的设计达到同一效果,以往是把折扣固化在具体业务对象里的,通过修改设计,我们将具体的折扣率进行封装和抽象,这使得程序更加灵活。从设计原则的角度来分析,把不同的折扣率分开进行封装,修改其一不会影响到其他的对象,符合“单一职责”吗?通过抽象,制定了统一的折扣计算接口,在程序中依赖于接口,通过配置文件,让接口和实现类之间的耦合关系进一步松散,这不正是OCP的思想吗?

这里也许会有朋友会说,此处并没有完全应用到策略模式,根据策略模式的定义:“准备一组算法,并将每一个算法封装起来,使得它们可以互换。”此处的Buy就扮演着模式的环境角色,持有有策略类(通常是抽象出的接口和抽象类--依赖于抽象)的引用,在这里,只是把引用(IBookStrategy)放置到了方法(TGetMoney)里,我们完全可以把他从方法里拿出来放到类地,个人而言,上面这种用法可当作是策略模式的一个演化。如下UML图:

同样,根据模式的定义,我们上面的设计没有做到“互换”,在本文前部分介绍策略模式的时候有分析,策略模式里,具体使用那一种策略是由用户发起的,而上面的设计却是把“互换”转移到了配置文件,这样使用的原因是我们并没有使用最本质的策略模式,而是把策略模式进行了演化,如果硬要让上面的设计更具备策略模式的味道,那我们可以进行如下修改,让Buy持有策略的引用,通过构造方法初始化策略对象;修改后的代码如下:

 1namespace EBook.Library
 2{
 3    public class Buy:Sell
 4    {
 5        /// <summary>
 6        /// 构造方法
 7        /// </summary>
 8        /// <param name="bType">书的类型</param>
 9        /// <param name="userName">用户名</param>
10        /// <param name="bookName">书名</param>
11        /// <param name="bookPrice">书的定价</param>

12        public Buy(B_Type bType, string userName, string bookName, double bookPrice,IBookStrategy strategy)
13            : base(userName, bookName, bookPrice)
14        {
15            _bType = bType;
16            this.book = strategy;
17        }

18
19        IBookStrategy book = null;
20
21        /// <summary>
22        /// 根据书的类型来定折扣
23        /// 当然,这里的折扣本来是应该从数据库或者配置文件中取的,我们演示就固化到这里。
24        /// </summary>
25        /// <returns></returns>

26        public override double TGetMoney()
27        {
28            string strategy = ConfigurationManager.AppSettings[_bType.ToString()];
29            string assembly = ConfigurationManager.AppSettings["assembly"];
30            book = (IBookStrategy)Assembly.Load(assembly).CreateInstance(strategy);
31            BookCash = BookPrice * book.GetRate();
32            return BookCash;
33        }

34
35
36        /// <summary>
37        /// 执行插入数据库的操作,但是我们这里不需要,只要把结果显示出来
38        /// 所以我们让他给我们返回一句话就OK了。
39        /// </summary>
40        /// <returns></returns>

41        public override string TExecute()
42        {
43            return string.Format("尊敬的会员:{0},您购买《{1}》,定价为:{2}元,折扣后为:{3}元",
44                UserName, BookName, BookPrice, BookCash);
45        }

46    }

47}

此时,客户在调用的时候就需要指定具体使用那一种策略了,如下:

IMoney m = new Buy(B_Type.LIFT, "beniao""ASP.NET"53.50,new LiftStrategy());

这也就达到了具体使用那一种策略又用户发起的需求了,此时的设计如下图:

一个小小的程序示例,通过不断的演化设计,使得程序在不断的演化过程中变得更加灵活。正应了那句“麻雀虽小,却五脏具全”,看来软件设计里有着太多的东西只得我们去学习的了。本文就介绍于此,详细请关注后续部分。

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。

资源网站: UML软件工程组织