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

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

 

在我上篇文章应用OOP的设计过程演化(一) 里,结合了实例通过应用OOP和重构等技术,你已看到代码是怎样一步一步复活的。让最初死板的代码变得灵活、可扩展,设计的不断演化过程证实,代码一步一步的复活就如同给一只冻僵翅膀的小鸟带去温暖的阳光一样。

上一篇文章虽然算得上是完美的演义了一个应用OOP的设计过程,但缺点也不少,可能因为这样给文章留下了败笔。那下面我们就来分析下这些不足之出。我们在设计中为什么要不断的抽象,重构?因为最初的设计不可能是完美的,我们不能一下子就完全把各个对象、类、接口等角色的职责划分得清清楚楚,只有通过不断的对设计进行修改才能更进一步的分清各个角色的职责。

“既然抽象销售业务的基类(Sel)l和租赁业务的基类(Hire)都具有相同的行为,这里我们完全可以在进一步的抽象,为什么不为这两个类定义一个统一的接口呢?”这是上一篇文章中留下的问题。是的,我们确实应该这么做:

 1/// <summary>
 2/// 系统总接口
 3/// </summary>

 4public interface IMoney
 5{
 6    /// <summary>
 7    /// 返回本次交易的金额
 8    /// </summary>
 9    /// <returns></returns>

10    double GetMoney();
11
12    /// <summary>
13    /// 执行某项特定操作(销售、出租、归还)
14    /// </summary>
15    /// <returns></returns>

16    string Execute();
17}

此时,我们还需要修改Sell和Hire两个类,让其继承IMoney接口,如下UML图示:

此时,客户端调用就可以直接依赖于最高层抽象IMoney接口,是不是到此就画上完美的句号了呢?事实并非我们所想的那么简单,我们虽然已经抽象出了最高层次的接口,但是这样还是有所不足,那不足之处在哪里呢?解决这个问题之前我们先来分析下具体的业务逻辑。

在一个书店的业务(不管是销售还是租借业务)里,只要存在业务关系,那就存在这这样的依赖,从第一篇文章(没有阅读过第一篇文章建议先阅读完第一篇文章:《书店信息系统》系列一----应用OOP的设计过程演化 )里的设计可以看出,每完成一笔业务交易,就会涉及到顾客类型(会员、普通顾客)、交易类型(出售、出租)、租借类型(租借、归还),我们还可以为书进行分类,比如生活类,小说类以及杂志等。既然有这样的关系存在,从设计上来说我们是不能在应用中强行来指定类型的,那应该怎么做呢?我们是不是应该对业务类型进行封装?这里我采用枚举:

 1/// <summary>
 2    /// 会员类型
 3    /// </summary>

 4    public enum U_Type
 5    {
 6        /// <summary>
 7        /// 会员
 8        /// </summary>

 9        MEMBER,
10
11        /// <summary>
12        /// 普通顾客
13        /// </summary>

14        SHOPPER
15    }

16
17    /// <summary>
18    /// 书的类型
19    /// </summary>

20    public enum B_Type
21    {
22        /// <summary>
23        /// 小说分类
24        /// </summary>

25        NOVEL,
26
27        /// <summary>
28        /// 生活百态
29        /// </summary>

30        LIFT,
31
32        /// <summary>
33        /// 杂志
34        /// </summary>

35        MAGAZINE
36    }

37
38    /// <summary>
39    /// 交易类型
40    /// </summary>

41    public enum S_Type
42    {
43        /// <summary>
44        /// 出售
45        /// </summary>

46        SELL,
47
48        /// <summary>
49        /// 出租
50        /// </summary>

51        HIRE
52    }

53
54    /// <summary>
55    /// 租借类型
56    /// </summary>

57    public enum H_Type
58    {
59        /// <summary>
60        /// 租借
61        /// </summary>

62        RENT,
63
64        /// <summary>
65        /// 归还
66        /// </summary>

67        BACK
68    }

对也类型进行封装后,我们在次进一步的分析具体的业务逻辑,在书店业务里,每次交易是不是还存在着书名、客户(顾客)名、书的定价以及顾客所支付的现金呢?然后这些属性都是任何一笔业务交易都存在的,从对象的职责上来说,我们应该把这些属性建立在共性层次上,那是这样的吗?

 1public abstract class Root:IMoney
 2{
 3    protected U_Type _uType;     //会员类型
 4    protected B_Type _bType;     //书的类型
 5    protected S_Type _sType;     //交易类型
 6
 7    protected string _userName;  //用户名
 8    protected string _bookName;  //书名
 9    protected double _bookPrice; //书的定价
10    //实际支付现金,不管是租书还是还书还是买书,他都会涉及到最终给了多少钱这个问题
11    protected double _bookCash;  
12
13    /// <summary>
14    /// 处理租赁或销售的操作
15    /// </summary>
16    /// <returns></returns>

17    public abstract string Execute();
18
19    /// <summary>
20    /// 返回本次交易的金额
21    /// </summary>
22    /// <returns></returns>

23    public abstract double GetMoney();
24}

从新建立了Root类,用来封装业务逻辑共享的属性,之前我们抽象出了最高层次的IMoney,Root除了封装属性外还应该具备共同的操作行为,而共同的行为已经定义在IMoney接口里,此时我们就可以一劳永逸地享受原有的设计了,只需要让Root继承于IMoney接口就OK。到此,系统的体系结构就算设计完成了。

应用体系设计完毕,那下面应该把全部精力投入到业务逻辑的分析上了。首先从销售逻辑出发,书店要销售出去一本书,那他首先要做的工作是什么?暴露书的属性:书名和定价还应该有买书的用户吧,其次还应该有客户所支付的现金。显然这些职责应该划分到销售书的父类(Sell)里,实际收取了多少钱这个还需要根据顾客的类型来决定具体采用何种收费策略,具体的收费策略的职责应该是具体的业务对象(Buy和SBuy)来完成,Sell作为父类,他所承担的职责是封装具体业务对象的共同属性和行为。详细如下:

 1namespace EBook.Step4
 2{
 3    /// <summary>
 4    /// 抽象出销售书的父类,所以的销售行为都继承于它。
 5    /// </summary>

 6    public abstract class Sell:Root
 7    {
 8        /// <summary>
 9        /// 初始化该类
10        /// </summary>
11        /// <param name="userName">用户名</param>
12        /// <param name="bookName">书名</param>
13        /// <param name="bookPrice">书的定价</param>

14        public Sell(string userName, string bookName, double bookPrice)
15        {
16            _userName = userName;
17            _bookName = bookName;
18            _bookPrice = bookPrice;
19        }

20
21        外露属性(用户名、书名、定价)
56
57        实现基类中的抽象方法以及将任务分派到下面的派生类去 
73
74        /// <summary>
75        /// 处理销售书的方法
76        /// </summary>

77        public abstract string TExecute();
78
79        /// <summary>
80        /// 返回本次交易的金额
81        /// </summary>

82        public abstract double TGetMoney();
83    }

84}

85

抽象业务层之下的具体业务对象,他门的职责就是完成具体的业务,根据我们之前的体系设计来分析,抽象销售业务(Sell)下有两个具体的业务对象(会员购书Buy和普通顾客购书SBuy),深入到具体的业务对象领域,之前我们为书分类了,那用户在购买书的时候在收费策略上肯定会判断书的类型,书的类型属性我们已经在抽象层Root里定义,这里我们只需要给他初始化下值就可以了(通过构造方法):

 1/// <summary>
 2/// 
 3/// </summary>
 4/// <param name="bType">书的类型</param>
 5/// <param name="userName">用户名</param>
 6/// <param name="bookName">书名</param>
 7/// <param name="bookPrice">书的定价</param>

 8public Buy(B_Type bType, string userName, string bookName, double bookPrice)
 9    : base(userName, bookName, bookPrice)
10{
11    _bType = bType;
12}

下面是具体的逻辑行为:

 1/// <summary>
 2/// 根据书的类型来定折扣
 3/// 当然,这里的折扣本来是应该从数据库或者配置文件中取的,我们演示就固化到这里。
 4/// </summary>
 5/// <returns></returns>

 6public override double TGetMoney()
 7{
 8    switch (_bType)
 9    {
10        case B_Type.NOVEL: BookCash = BookPrice * 0.9;
11            break;
12        case B_Type.LIFT: BookCash = BookPrice * 0.7;
13            break;
14        case B_Type.MAGAZINE: BookCash = BookPrice * 0.8;
15            break;
16    }

17    return BookCash;
18}

19
20/// <summary>
21/// 执行插入数据库的操作,但是我们这里不需要,只要把结果显示出来
22/// 所以我们让他给我们返回一句话就OK了。
23/// </summary>
24/// <returns></returns>

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

普通顾客的逻辑于会员的逻辑差不多,只是在收费的策略上有所不同,主要体现在折扣上。

 1/// <summary>
 2/// 普通顾客购书
 3/// </summary>

 4public class SBuy:Sell
 5{
 6    public SBuy(B_Type bType, string userName, string bookName, double bookPrice)
 7        : base(userName, bookName, bookPrice)
 8    {
 9        _bType = bType;
10    }

11
12    public override double TGetMoney()
13    {
14        switch (_bType)
15        {
16            case B_Type.NOVEL: BookCash = BookPrice * 0.8;
17                break;
18            case B_Type.LIFT: BookCash = BookPrice * 0.4;
19                break;
20            case B_Type.MAGAZINE: BookCash = BookPrice;  //不打折
21                break;
22        }

23        return BookCash;
24    }

25
26    public override string TExecute()
27    {
28        return string.Format("尊敬的顾户:{0},您购买《{1}》,定价为:{2}元,折扣后为:{3}元",
29            UserName, BookName, BookPrice, BookCash);
30    }

31}

此时的结构体系就应该是这样的:

销售业务分析完毕,接下来我们来看看租借业务的实现。在租借业务里存在着两种业务三个业务对象:出租,归还(会员和普通顾客),首先来分析出租的业务逻辑,我们回想到现实生活中的租书业务,租书的时候是不需要支付租金的,但是需要支付押金,而收取押金需要根据租借时间(天数)来计算。也就是说,在租借业务里出了从继承体系中继承而来的属性外,我们不得不在另外添加两个属性:租借天数和所交押金;这是租借(出租和归还)业务所共有的:

1private int _day;
2private double _deposit;

同样,我们还得对外暴露属性,包括用户名、书名、定价、租借天数、押金和实收现金:

 1/// <summary>
 2/// 用户名
 3/// </summary>

 4public string UserName
 5{
 6    get return _userName; }
 7}

 8
 9/// <summary>
10/// 书名
11/// </summary>

12public string BookName
13{
14    get return _bookName; }
15}

16
17/// <summary>
18/// 书的定价
19/// </summary>

20public double BookPrice
21{
22    get return _bookPrice; }
23}

24
25/// <summary>
26/// 租借天数
27/// </summary>

28public int Day
29{
30    get return _day; }
31    set { _day = value; }
32}

33
34/// <summary>
35/// 押金
36/// </summary>

37public double Deposit
38{
39    get return _deposit; }
40    set { _deposit = value; }
41}

42
43/// <summary>
44/// 实收现金
45/// </summary>

46public double BookCash
47{
48    get return _bookCash; }
49    set { _bookCash = value; }
50}

在具体的业务行为上和销售行为没有什么区别,详细定义如下:

 1实现基类中的抽象方法以及将任务分派到下面的派生类去
18
19/// <summary>
20/// 处理租赁书的方法
21/// </summary>

22public abstract string TExecute();
23
24/// <summary>
25/// 返回本次交易的金额
26/// </summary>

27public abstract double TGetMoney();

实现基类的抽象方法,但是考虑到还需要再下级的派生类来完成,所以我们选择让他调用其他能够被派生类修改的方法,这也就把具体的逻辑派生到具体的业务对象去实现了,这里的具体业务对象也就是租借业务对象(Rent)、会员归还业务对象(MBack)和普通顾客归还业务对象(SBack)。
租借业务对象(Rent):

 1namespace EBook.Step4
 2{
 3    /// <summary>
 4    /// 租书
 5    /// 分析:租书的时候是不需要支付租金的,但是需要支付押金
 6    /// </summary>

 7    public class Rent:Hire
 8    {
 9        /// <summary>
10        /// 初始化该类
11        /// </summary>
12        /// <param name="userName">用户名</param>
13        /// <param name="bookName">书名</param>
14        /// <param name="bookPrice">书的定价</param>
15        /// <param name="deposit">押金</param>
16        /// <param name="bookCash">实际支付</param>

17        public Rent(string userName, string bookName, double bookPrice, double bookCash)
18        {
19            _userName = userName;
20            _bookName = bookName;
21            _bookPrice = bookPrice;
22            Deposit = bookCash;  //押金也就是租书的时候实际支付的现金
23            _bookCash = bookCash;
24        }

25
26        /// <summary>
27        /// 执行出租逻辑
28        /// </summary>

29        public override string TExecute()
30        {
31            return string.Format("尊敬的顾客:{0},您租借《{1}》,本书定价为:{2}元,你支付押金:{3}元,实际支付{4}元",
32                UserName, BookName, BookPrice, Deposit, BookCash);
33        }

34
35        /// <summary>
36        ///返回本笔交易的金额
37        /// </summary>

38        public override double TGetMoney()
39        {
40            //直接返回实际支付的现金
41            return BookCash;
42        }

43    }

44}

会员归还业务对象(MBack):还书的时候需要退还押金并支付租金,我们直接把租金在押金里面硬性扣除.不同的是这个是会员还书,所以租金和普通顾客的租金有区别。

 1namespace EBook.Step4
 2{
 3    /// <summary>
 4    /// 会员还书
 5    /// 分析:还书的时候需要退还押金并支付租金,我们直接把租金在押金里面硬性扣除.
 6    /// 不同的是这个是会员还书,所以租金和普通顾客的租金有区别.
 7    /// </summary>

 8    public class MBack:Hire
 9    {
10        /// <summary>
11        /// 初始化对象
12        /// </summary>
13        /// <param name="userName">用户名</param>
14        /// <param name="bookName">书名</param>
15        /// <param name="day">租借天数</param>
16        /// <param name="bType">图书类型</param>

17        public MBack(string userName, string bookName, int day, B_Type bType, double deposit)
18        {
19            _userName = userName;
20            _bookName = bookName;
21            Day = day;
22            _bType = bType;
23            //实际开发中这里的租金应该是在借书的时候交的租金,这里是做演示就写死在这里了。
24            Deposit = deposit;
25        }

26
27        /// <summary>
28        /// 执行还书的操作逻辑
29        /// </summary>

30        public override string TExecute()
31        {
32            return string.Format("尊敬的会员,您租借《{0}》,共计:{1}天,已支付押金{2}元,实际产生租金{3}元,应找回您{4}元。",
33                BookName, Day, Deposit, GetRent(), TGetMoney());
34        }

35
36        /// <summary>
37        /// 直接应该退给顾客多少钱
38        /// </summary>

39        public override double TGetMoney()
40        {
41            //直接返回应找回的现金,直接把租金在押金里面硬性扣除
42            return Deposit - GetRent();//押金减去租金=退还给顾客的钱
43        }

44
45        /// <summary>
46        /// 计算书的租金
47        /// </summary>
48        /// <returns></returns>

49        private double GetRent()
50        {
51            switch (_bType)
52            {
53                case B_Type.NOVEL: BookCash = Convert.ToDouble(Day) * 0.1;
54                    break;
55                case B_Type.LIFT: BookCash = Convert.ToDouble(Day) * 0.5;
56                    break;
57                case B_Type.MAGAZINE: BookCash = Convert.ToDouble(Day) * 0.3;
58                    break;
59            }

60            return BookCash;
61        }

62    }

63}

普通顾客归还业务对象(SBack):还书的时候需要退还押金并支付租金,我们直接把租金在押金里面硬性扣除,和会员归还业务对象没什么大区别,只是在租金的算法上有点差异而已:

 1namespace EBook.Step4
 2{
 3    /// <summary>
 4    /// 普通顾客还书
 5    /// 分析:还书的时候需要退还押金并支付租金,我们直接把租金在押金里面硬性扣除
 6    /// </summary>

 7    public class SBack:Hire
 8    {
 9        /// <summary>
10        /// 初始化对象
11        /// </summary>
12        /// <param name="userName">用户名</param>
13        /// <param name="bookName">书名</param>
14        /// <param name="day">租借天数</param>
15        /// <param name="bType">图书类型</param>

16        public SBack(string userName, string bookName, int day, B_Type bType, double deposit)
17        {
18            _userName = userName;
19            _bookName = bookName;
20            Day = day;
21            _bType = bType;
22            Deposit = deposit;
23        }

24
25        /// <summary>
26        /// 执行还书的操作逻辑
27        /// </summary>
28        /// <returns></returns>

29        public override string TExecute()
30        {
31            return string.Format("尊敬的顾客,您租借《{0}》,共计:{1}天,已支付押金{2}元,实际产生租金{3}元,应找回您{4}元。",
32                BookName, Day, Deposit, GetRent(), TGetMoney());
33        }

34
35        /// <summary>
36        ///直接应该退给顾客多少钱
37        /// </summary>

38        public override double TGetMoney()
39        {
40            //直接返回应该找零的现金,已交的押金减去实际的租金
41            return Deposit - GetRent();
42        }

43
44        /// <summary>
45        /// 计算租金
46        /// </summary>
47        /// <returns></returns>

48        private double GetRent()
49        {
50            switch (_bType)
51            {
52                case B_Type.NOVEL: BookCash = Convert.ToDouble(Day) * 0.1;
53                    break;
54                case B_Type.LIFT: BookCash = Convert.ToDouble(Day) * 1d;
55                    break;
56                case B_Type.MAGAZINE: BookCash = Convert.ToDouble(Day) * 0.5;
57                    break;
58            }

59            return BookCash;
60        }

61    }

62}

到此为止,整个系统的体系结构设计也业务逻辑实现都已经完成,我们可以于此画上个“句号”了。下面来写个程序简单测试下这五个具体业务对象。

 1namespace EBook.Step4
 2{
 3    class Program
 4    {
 5        static void Main(string[] args)
 6        {
 7            Root root = new Buy(B_Type.LIFT, "beniao""Design Pattern"50.30);
 8            Console.WriteLine("本次交易金额为:" + root.GetMoney());
 9            Console.WriteLine( root.Execute());
10
11            Console.WriteLine("\n--------------------------------------------\n");
12
13            root = new SBuy(B_Type.MAGAZINE, "beniao""Design Pattern"50.30);
14            Console.WriteLine("本次交易金额为:" + root.GetMoney());
15            Console.WriteLine(root.Execute());
16
17            Console.WriteLine("\n--------------------------------------------\n");
18
19            root = new Rent("beniao""Design Pattern"38.60,100.00);
20            Console.WriteLine("本次交易金额为:" + root.GetMoney());
21            Console.WriteLine(root.Execute());
22
23            Console.WriteLine("\n--------------------------------------------\n");
24            root = new MBack("beniao""C#"5, B_Type.LIFT,100.00);
25            Console.WriteLine("本次交易金额为:" + root.GetMoney());
26            Console.WriteLine(root.Execute());
27
28            Console.WriteLine("\n--------------------------------------------\n");
29            root = new SBack("beniao""C#"5, B_Type.LIFT,100.00);
30            Console.WriteLine("本次交易金额为:" + root.GetMoney());
31            Console.WriteLine(root.Execute());
32
33        }

34    }

35}

测试结果如下图:

本文在原有的设计基础上又进行了修改,抽象出了抽象父类(Root)和总接口(IMoney),最终的设计如下示:

我们之前在设计的过程中已经抽象出了顶层接口IMoney,那么在客户端里我们可以直接使用IMoney接口来代替所有的抽象类,就上面的简单测试程序里,我们完全可以使用IMoney接口来代替Root,关于这点我将在后续文章里详细介绍,本文就介绍于此,我相信之前的设计过程演变+案例代码的展示+你自己学习后的总结会比我解说得更好。

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

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