<!-- [if gte mso 9]><xml><w:WordDocument><w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel><w:DisplayHorizontalDrawingGridEvery>0</w:DisplayHorizontalDrawingGridEvery><w:DisplayVerticalDrawingGridEvery>2</w:DisplayVerticalDrawingGridEvery><w:DocumentKind>DocumentNotSpecified</w:DocumentKind><w:DrawingGridVerticalSpacing>7.8</w:DrawingGridVerticalSpacing><w:View>Normal</w:View><w:Compatibility></w:Compatibility><w:Zoom>0</w:Zoom></w:WordDocument></xml><![endif]-->
应用OOP 的设计过程演化
面向对象的程序设计(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(" 会员还书 ");