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

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

 

面向对象的程序设计(Object-Oriented Programming,简记为OOP)立意于创建软件重用代码,具备更好地模拟现实世界环境的能力,这使它被公认为是自上而下编程的优胜者。它通过给程序中加入扩展语句,把函数“封装”进编程所必需的“对象”中。面向对象的编程语言使得复杂的工作条理清晰、编写容易。

在计算时代的早期,程序员基于语句思考编程问题。到了20世纪七八十年代,程序员开始基于子程序去思考编程。进入21世纪,程序员以类为基础思考编程问题。而类是OOP中的核心组成元素,通常都是使用类来“封装”对象(属性、行为)。在经典图书《代码大全》里定义:“创建高质量的类,第一步,可能也是最重要的一步,就是创建一个好的接口。这也包括了创建一个可以通过接口来展现的合理的抽象,并确保细节仍被隐藏在抽象背后。”

为了更好的理解设计思想,本系列文章以简单的《书店信息系统》为例,但随着需求的增加,程序将越来越复杂。此时就有修改设计的必要,重构和设计模式就可以派上用场了。最后当设计渐趋完美后,你会发现,即使需求不断增加,你也可以神清气闲,不用为代码设计而烦恼了。

在一个书店里,主要业务就是销售书,销售书后所得到的就是收取到的资金(本次交易金额),那以这个业务来分析,在不考虑设计的情况下,我们该怎么去实现:

 1namespace EBook.Step1
 2{
 3    /// <summary>
 4    /// 会员购书
 5    /// </summary>

 6    public class Buy
 7    {
 8        /// <summary>
 9        /// 处理销售书的方法
10        /// </summary>

11        public void Execute()
12        {
13            Console.WriteLine("会员购买了一本书");
14        }

15
16        /// <summary>
17        /// 买书得到了多少钱
18        /// </summary>

19        public void GetMoney()
20        {
21            Console.WriteLine("收到了xx.xx元RMB");
22        }

23    }

24}

这是针对书店的会员购书的业务逻辑,那如果是普通的顾客来购书呢?此时我们不得不为普通的顾客提供专门的服务(建立普通顾客业务逻辑类):

 1namespace EBook.Step1
 2{
 3    /// <summary>
 4    /// 普通顾客购书
 5    /// </summary>

 6    public class SBuy
 7    {
 8        public void Execute()
 9        {
10            Console.WriteLine("普通顾客购买了一本书");
11        }

12
13        public void GetMoney()
14        {
15            Console.WriteLine("收到了xx.xx元RMB");
16        }

17    }

18}

而客户端通过判断顾客的类型来决定调用具体的类来处理相应的操作:

 1namespace EBook.Step1
 2{
 3    class Program
 4    {
 5        static void Main(string[] args)
 6        {
 7            string uType = Console.ReadLine();
 8            switch (uType)
 9            {
10                case "会员": Member(); break;
11                case "普通顾客": General(); break;
12            }

13        }

14
15        private static void General()
16        {
17            SBuy sbuy = new SBuy();
18            sbuy.Execute();
19            sbuy.GetMoney();
20        }

21
22        private static void Member()
23        {
24            Buy buy = new Buy();
25            buy.Execute();
26            buy.GetMoney();
27        }

28    }

29}

仔细分析这段代码,虽然我们已经应用了OO的思想,将不同的顾客分为不同的对象来处理,但这样的设计同样很糟糕。也许你已经看出,这糟糕之处就是在switch这里。

不错,如果书店的客户不只是上述所提到的两种类型,还有如黄金会员,白金会员,白银会员,普通会员......等等一系列的划分,随着业务的扩展,将来会员类型也许还会不断的增加,那么就会去修改switch不断的增加相应的会员处理逻辑,然后让switch子句越来越长,直至达到你需要无限的拉动滚动条才能看到switch的结束。如上设计的UML图如下:

在上面的设计中,我们已经应用到了OO的思想,把不同个顾客类型做为一独立的对象来处理。仔细观察,会员(Buy)和普通顾客具有完全相同的方法,为什么不为它们建立一个共同的父类呢?

通过共性的抽象,让会员和普通顾客都去继承并实现父类的抽象方法,那代码是这样的吗?

 1/// <summary>
 2/// 抽象出销售书的父类,所以的销售行为都继承于它。
 3/// </summary>

 4public abstract class Sell
 5{
 6    /// <summary>
 7    /// 处理销售书的方法
 8    /// </summary>

 9    public abstract void Execute();
10
11    /// <summary>
12    /// 卖书得到了多少钱
13    /// </summary>

14    public abstract void GetMoney();
15}

16-----------------------------------------------
17/// <summary>
18/// 会员购书
19/// </summary>

20public class Buy:Sell
21{
22    public override void Execute()
23    {
24        Console.WriteLine("会员购买了一本书");
25    }

26
27    public override void GetMoney()
28    {
29        Console.WriteLine("收到了xx.xx元RMB");
30    }

31}

32----------------------------------------------
33/// <summary>
34/// 普通顾客购书
35/// </summary>

36public class SBuy:Sell
37{
38    public override void Execute()
39    {
40        Console.WriteLine("普通顾客购买了一本书");
41    }

42
43    public override void GetMoney()
44    {
45        Console.WriteLine("收到了xx.xx元RMB");
46    }

47}

我们通过抽象,引用了继承的思想,使整个设计也有了OOP的味道。

然而从现实生活中来分析,销售逻辑是一个抽象层,而我们所针对的是具体的顾客类型,出了在程序里应用多态特性,Sell类并没有实际使用的情况,这就是为何将其设计为抽象类及抽象方法,而不是使用普通的类里定义虚方法(virtual)的方式来实现。对应在设计中,就是:这个类永远不会被实例化,实例化的是它的子类。

此时,客户端的调用可以直接依赖于抽象层(Sell),不过这样的设计实质并没有多大的变化,客户端还是需要通过判断决定该调用那一个具体的实现。

 1public class Resolve
 2{
 3    /// <summary>
 4    /// //依赖于抽象
 5    /// </summary>
 6    /// <param name="sell"></param>

 7    public void Execute(Sell sell)
 8    {
 9        sell.Execute();
10        sell.GetMoney();
11    }

12}

13
14namespace EBook.Step2
15{
16    //依赖于抽象,有了继承,有了OO的味道。
17    class Program
18    {
19        static void Main(string[] args)
20        {
21            //会员
22            new Resolve().Execute(new Buy());
23            Console.WriteLine("\n-----------------------------\n");
24            //普通顾客
25            new Resolve().Execute(new SBuy());
26        }
 
27    }

28}

29

这里我们先不谈客户端调用去判断顾客的类型。从现在的设计来看,即满足了类之间的层次关系,同时又保证了类的最小化原则,更利于扩展。即使你现在要增加如黄金会员(Gold)和白金会员(Platinum)等会员类型,只需要设计Gold和Platinum类,并继承Sell,重写Execute和GetMoney方法即可,而Resolve类对象的Execute方法根本就不用改变。

针对如上的设计来说,完全可以满足一个简单的销售逻辑的处理,可算是一个完美的设计。然而,在我们的实际项目中会有很多意想不到的事发生,其中需求变更应该是最为头疼的。刁钻的客户是永远不会满足的,这意味着我们就不能给我们的设计画上圆满的句号。时间久了,书店的一些书籍早已因陈旧而不能销售出去,可老板又不想让这些书成为废品,书无论是新还是旧都有他的价值所在,旧书的里的知识或许是不能与新版的书籍比配,但还是有一定的参考价值,就如我们去研究历史一样,是为了什么?是为了更好的迎接未来。

书店的业务扩展,老板决定将陈旧的书籍用来出租(呵呵,这想法不错,满足了像我这样的穷人想看书可又没钱买书的XX,UPUP.....),根据我们上面在设计销售经验来看,那出租我们应该怎么来设计呢?是不是也应该把不同的对象做为的独立的逻辑来处理呢?答案是肯定的,那到底要怎么去设计呢,这要求我们深入到具体的业务逻辑了。

通过分析现实中的业务逻辑,出租主要涉及到两个方面:租借和归还。而我们上面的设计中把顾客分为了会员和普通顾客两类,那么归还是不是应该划分为会员还书和普通顾客换书呢?这是肯定的,因为会员和普通顾客在租书的租金上是不一样的,会员和普通顾客在租金上应该是两种不同的策略。

从上面的分析得出,出租主要分为租借、会员归还和普通顾客归还这三种类型的逻辑。而租书不用给租金但必须先交押金,还会则需要收取租金(可从押金中扣除)。也就是说这三种类型里都回有处理出租(租借和归还)和交易金额的逻辑。既然都有共性,那也应该抽象出父类,是这样设计的吗?

 1namespace EBook.Step3
 2{
 3    /// <summary>
 4    /// 作为租赁业务的一个基类,所以的租赁行为都继承于它。
 5    /// </summary>

 6    public abstract class Hire
 7    {
 8        /// <summary>
 9        /// 处理租赁书的方法
10        /// </summary>

11        public abstract void Execute();
12
13        /// <summary>
14        /// 租书所得到的租金
15        /// </summary>

16        public abstract void GetMoney();
17    }

18}

我们来看看UML草图:

 1/// <summary>
 2/// 租书
 3/// 分析:租书的时候是不需要支付租金的,但是需要支付押金
 4/// </summary>

 5public class Rent:Hire
 6{
 7    /// <summary>
 8    /// 执行出租逻辑
 9    /// </summary>

10    public override void Execute()
11    {
12        Console.WriteLine("租出一本XXX书");
13    }

14
15    /// <summary>
16    /// 计算出租后所得到的租金
17    /// </summary>

18    public override void GetMoney()
19    {
20        Console.WriteLine("得到了XX.XX元的租金");
21    }

22}

 
 1/// <summary>
 2/// 还书
 3/// 会员还书--租金和普通顾客的租金有区别
 4/// </summary>

 5public class MBack:Hire
 6{
 7    /// <summary>
 8    /// 执行还书逻辑
 9    /// </summary>

10    public override void Execute()
11    {
12        Console.WriteLine("会员还书");
13    }

14
15    /// <summary>
16    /// 计算会员租书的租金
17    /// </summary>

18    public override void GetMoney()
19    {
20        Console.WriteLine("会员租金打5折");
21    }

22}

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

 4public class SBack:Hire
 5{
 6    public override void Execute()
 7    {
 8        Console.WriteLine("普通顾客还书");
 9    }

10
11    /// <summary>
12    /// 计算普通顾客租书的租金
13    /// </summary>

14    public override void GetMoney()
15    {
16        Console.WriteLine("普通顾客租金打8折");
17    }

18}

此时,在Resolve类里就需要通过业务类型(销售或出租)、用户类型(会员或普通顾客)和出租类型(租借或归还)的不同层次的判断,然后去执行相应的具体逻辑实现。

 1public class Resolve
 2{
 3    /// <summary>
 4    /// 
 5    /// </summary>
 6    /// <param name="sType">销售类型</param>
 7    /// <param name="uType">用户类型</param>
 8    /// <param name="rType">出租类型</param>

 9    public void Execute(string sType,string uType,string rType)
10    {
11        switch (sType)
12        {
13            case "销售"if (uType == "会员")
14                {
15                    Sell(new Buy());
16                }

17                else
18                {
19                    Sell(new SBuy());
20                }
break;
21            case "出租"if (rType == "租借")
22                {
23                    Hire(new Rent());
24                }

25                else
26                {
27                    if (uType == "会员") Hire(new MBack());
28                    else Hire(new SBack());
29                }
break;
30        }

31    }

32
33    private void Sell(Sell sell)
34    {
35        sell.Execute();
36        sell.GetMoney(); //本次交易的金额
37    }

38
39    private void Hire(Hire hire)
40    {
41        hire.Execute();
42        hire.GetMoney();
43    }

44}

可以看到,上面的Resolve类里的方法定义特别的复杂,switch和if....else语句使整个设计显得太过迂腐,破坏了设计之美。这里需要怎么改善代码,使得干净利落呢?这里我们先不谈使用switch和if......else造成的迂腐设计,在后续文章里我会详细的介绍怎么搞定这个坏点。

现在可以总结一下,从Resolve类的演变,我们可以得出这样一个结论:在调用类对象的属性和方法时,尽量避免将具体类对象作为传递参数,而应传递其抽象对象,更好地是传递接口,将实际的调用和具体对象完全剥离开,这样可以提高代码的灵活性。

如public void Sell(Sell sell){};方法,我们就是使用的高层抽象Sell(销售行为的父类)作为参数类型,而在实际调用中则传递的是具体的实现子类。这种遵循依赖于抽象而不依赖于具体实现的设计原则,让设计更具灵活性。上述设计的UML草图如下:

仔细观察会发现,Sell和Hire都具有相同的行为,这里我们完全可以在进一步的抽象,为这两个类定义一个统一的接口,详细本文就不做介绍,我已经把内容安排到下一篇文章里,大家可以关注本系列的后续文章。

本文主要目的是讲述OOP的设计过程演化,在实例上没有做详细的业务逻辑处理;我相信看到我这句话的朋友是认真的阅读完了本文的全部内容,通过对代码一步一步的修改重构,应用OOP的设计思想,抽象继承来演义OOP的设计过程,至于本文的价值所在,我想看过本文的朋友心里都有个数。

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

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