posts - 101,  comments - 480,  trackbacks - 44
      在上一篇文章里(应用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方法就可以直接读取配置文件然后