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

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

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

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

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

 1 namespace  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 }

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

 1 namespace  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 }

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

 1 namespace  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>

 4 public   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>

20 public   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>

36 public   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),不过这样的设计实质并没有多大的变化,客户端还是需要通过判断决定该调用那一个具体的实现。

 1 public   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
14 namespace  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.....),根据我们上面 在设计销售经验来看,那出租我们应该怎么来设计呢?是不是也应该把不同的对象做为的独立的逻辑来处理呢?答案是肯定的,那到底要怎么去设计呢,这要求我们 深入到具体的业务逻辑了。

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

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

 1 namespace  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>

 5 public   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>

 5 public   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>

 4 public   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类里就需要通过业务类型(销售或出租)、用户类型(会员或普通顾客)和出租类型(租借或归还)的不同层次的判断,然后去执行相应的具体逻辑实现。

 1 public   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的设计过程,至于本文的价值所在,我想看过本文的朋友心里都有个数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tof21

支持原创

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值