软件设计模式与原则2

9.2软件的设计原则

9.2.1Solid原则介绍

         “Solid原则”代表软件设计过程中常见的五大原则,分别为:

(1)S:单一职责原则(Single Responsibility Principle):

         一个类应该只负责一个(种)事情;

(2)O:开闭原则(Open Closed Principle):

         优先选择在已有的类型基础上扩展新的类型,避免修改已有类型(已有代码);

(3)L:里氏替换原则(Liskov Substitution Principle):

         任何基类出现的地方,派生类一定可以代替基类出现,言下之意就是,派生类一定要具备基类的所有特性;

(4)I:接口隔离原则(Interface Segregation Principle):

         一个类型不应该去实现它不需要的接口,换句话说,接口应该只包含同一类方法或属性等;

(5)D:依赖倒置原则(Dependency Inversion Principle):

         高层模块不应该依赖于低层模块,高层模块和低层模块应该同时依赖于一个抽象层(接口层)。

         设计模式相对来讲更具体,每种设计模式几乎都能解决现实生活中某一具体问题,而设计原则相对来讲更抽象,它是我们在软件设计过程中的行为准则,并不能用在某一具体情景之中。以上五大原则单从字面上理解起来不太直观,下面依次举例说明之。

9.2.2单一职责原则(SRP)

         “一个类应该只负责一个(种)事情”,原因很简单,负责的事情越多,那么这个类型出错或者需要修改的概率越大,假如现在有一个超市购物的会员类VIP:

//Code 9-5
class VIP:IData
{
         publicvoid Read()
         {
                   try
                   {
                            //readdb here…
                   }
                   catch(Exceptionex)
                   {
                            System.IO.File.WriteAllText(@"c:\errorlog.txt",ex.ToString());  //NO.1
                   }
         }
}
interface IData  //NO.2
{
         voidRead();
}

如上代码Code 9-5所示,定义了一个访问数据库的IData接口(NO.2处),该接口包含一个Read方法,用来读取会员信息,会员类VIP实现了IData接口,在编写Read方法时,我们捕获访问数据库的异常后,直接将错误信息写入到了日志文件(NO.1处)。这段代码看似没有任何问题,但是后期确会暴露出设计不合理的现象,如果我们现在不想把日志文件输出到本地C盘(NO.1处),而是输出到D盘,那我们需要修改VIP的源码,没错,本来我们只是想修改日志部分的逻辑,现在却不得不更改VIP类的代码。出现这种现象的原因就是VIP类干了本不应该它干的事情:记录日志。就像下面这张图描述的:

图9-3 一个负责了太多事情的工具

如上图9-3所示,一把包含太多功能的刀,如果哪天某个功能坏掉,我们不得不将整把刀送去维修。正确解决以上问题的做法就是将日志逻辑与VIP类分开,代码如下:

//Code 9-6
class Logger //NO.1
{
         publicvoid WriteLog(string error)
         {
                   System.IO.File.WriteAllText(@"c:\errorlog.txt",error);
         }
}
class VIP:IData
{
         privateLogger _logger = new Logger();  //NO.2
   public void Read()
    {
       try
       {
           //read db here…
       }
       catch (Exception ex)
       {
           _logger.WriteLog(ex.ToString()); //NO.3
       }
    }
}

如上代码Code 9-6所示,我们定义了一个类型Logger专门负责记录日志(NO.1处),在VIP类中通过Logger类型来记录错误信息(NO.2和NO.3处),这样一来,当我们需要修改日志部分的逻辑时,不需要再动VIP类的代码。

         单一职责原则提倡我们将复杂的功能拆分开来,分配到每个单独的类型当中,至于什么是复杂的功能,到底将功能拆分到什么程度,这个是没有标准的,如果记录日志是一个繁琐的过程(本小节示例代码相对简单),你还可以将日志类Logger的功能再继续拆分。

9.2.3开闭原则(OCP)

         “优先选择在已有的类型基础上扩展新的类型,避免修改已有类型(已有代码)”,修改已有代码就意味着需要重新测试原有的功能,因为任何一次修改都可能影响已有功能。如果在普通VIP顾客的基础之上,多了白银会员(silver vip)顾客,这两种顾客在购物时的折扣不一样,如果VIP类定义如下(不全):

//Code 9-7
class VIP:IData
{
         privateint _viptype;  //vip type  NO.1
         //…
         publicvirtual void Read()
         {
                   //…
         }
         publicdouble GetDiscount(double totalSales)
         {
                   if(_viptype== 1)  //vip
                   {
                            returntotalSales – 10;  //NO.2
                   }
                   else  //silver vip
                   {
                            returntotalSales – 50;  //NO.3
                   }
         }
}

如上代码Code 9-7所示,我们在定义VIP类的时候,使用_viptype字段来区分当前顾客是普通VIP还是白银VIP(NO.1处),在打折方法GetDiscount中,根据不同的VIP种类返回不同打折后的价格(NO.2和NO.3处),这段代码的确也可以运行的很好,但是后期还是会暴露出设计不合理的地方,如果现在不止增加一个白银会员,还增加了一个黄金会员(gold vip),那么我们不得不再去修改GetDiscount方法中的if/else块,修改意味着原有功能可能会出现bug,因此我们不得不再去测试之前所有使用到了VIP这个类型代码。出现这个问题的主要原因就是我们从一开始设计VIP类的时候就不合理:没有考虑到将来可能会有普通会员的衍生体出现。

         如果我们一开始在设计VIP类的时候就应用了面向对象思想,我们的VIP类可以这样定义:

//Code 9-8
interface IDiscount  //NO.1
{
         doubleGetDiscount(double totalSales);
}
class VIP:IData,IDiscount
{
         //…
         publicvirtual void Read()
         {
                   //…
         }
         publicvirtual double GetDiscount(double totalSales) //NO.2
         {
                   returntotalSales – 10;
         }
}
class SilverVIP:VIP
{
         //…
         publicoverride double GetDiscount(double totalSales)
         {
                   returntotalSales – 50;  //NO.3
         }
}
class GoldVIP:SilverVIP
{
         //…
         publicoverride double GetDiscount(double totalSales)
         {
                   returntotalSales – 100;  //NO.4
         }
}

如上代码Code 9-8所示,我们定义了一个IDiscount的接口(NO.1处),包含一个打折的GetDiscount方法,接下来让VIP类实现了IDiscount接口,将接口中的GetDiscount方法定义为虚方法(NO.2处),后面的白银会员(SilverVIP)继承自VIP类、黄金会员(GoldVIP)继承自SilverVIP类,并分别重写GetDiscount虚方法,返回相应的打折之后的总价格(NO.3和NO.4处)。这样一来,新增加会员类型不需要去修改VIP类,也不影响之前使用了VIP类的代码。

         下图9-4显示了重新设计VIP类的前后区别:

图9-4 继承发生之后

如上图9-4所示,图中左边部分表示不采用继承的方式去实现普通VIP、白银VIP和黄金VIP的打折逻辑,可以看出,每次需要增加一种会员时,都必须去修改VIP类的代码,图中右边部分表示采用继承方式之后,每种会员均定义成一个类型,每个类型均可以负责自己的打折逻辑,以后不管新增多少种会员,均可以定义新的派生类,在派生类中定义新的打折逻辑。

         注:派生类中只需要重写打折的逻辑,不需要重新去定义读取数据库的逻辑,因为这个逻辑在基类和派生类中并没有发生变化。

9.2.4里氏替换原则(LSP)

         “任何基类出现的地方,派生类一定可以代替基类出现,言下之意就是,派生类一定要具备基类的所有特性”,意思就是说,如果B是A的儿子,那么B一定可以代替A去做任何事情,否则,B就不应该是A的儿子。我们在设计类型的时候,往往不去注意一个类型是否真的应该去继承另外一个类型,很多时候我们只是为了遵从所谓的“OO”思想。如果现在有一个管理员类Manager,因为管理员也需要读取数据库,所以我们让它继承自VIP类,代码如下:

//Code 9-9
class Manager:VIP
{
         //…
         publicoverride void Read()
         {
                   //…
         }
         publicoverride double GetDiscount(double totalSales)
         {
                   thrownew Exception(“don't have this function!”); //NO.1
         }
}

如上代码Code 9-9所示,我们定义Manager类,让其继承自VIP类,由于Manager类并没有“打折扣”的逻辑,因此我们重写GetDiscount方法时,抛出“don't have this function!”这样的异常(NO.1处),接下来我们可能编写出如下这样的代码:

//Code 9-10
List<VIP> vips = newList<VIP>();  //NO.1
vips.Add(new VIP());
vips.Add(new SilverVIP());
vips.Add(new GoldVIP());
vips.Add(new Manager());
//…
foreach(VIP v in vips)
{
         /…
         doubled = v.GetDiscount(…);  //NO.2
         //…
}

如上代码Code 9-10所示,我们定义了一个VIP类型的容器(NO.1处),依次将VIP、SilverVIP、GoldVIP以及Manager类型对象加入容器,最后通过foreach遍历该容器,调用容器中每个元素的GetDiscount方法(NO.2处),此段代码一切正常通过编译,因为编译器承认“基类出现的地方,派生类一定能够代替其出现”,但事实上,程序运行之后,在调用Manager类对象的GetDiscount虚方法时会抛出异常,造成这个现象的主要原因就是,我们根本没搞清楚类的继承关系,Manager类虽然也要访问数据库,但是它并非属于VIP的一种,也就是说,Manager类不应该是VIP类的儿子,如下图9-5:

 

图9-5Manager错误的继承关系

如上图9-5所示,Manager类虽然需要读取数据库,但是它并不需要有与“折扣”相关的操作,而且它根本不属于一种VIP的衍生物,正确的做法是让Manager类直接实现IData接口即可,如下代码:

//Code 9-11
class Manager:IData
{
         //…
         publicvoid Read()
         {
                   //…
         }
}

如上代码Code 9-11所示,Manager实现了IData接口之后,不再跟VIP类有关联,这样一来,前面Code 9-10代码在编译时,就会通不过,

//Code 9-12
List<VIP> vips = newList<VIP>();  //NO.1
vips.Add(new VIP());
vips.Add(new SilverVIP());
vips.Add(new GoldVIP());
vips.Add(new Manager());  //NO.2

如上代码Code 9-12所示,编译器会在NO.2处报错,原因很简单,Manager既然不是VIP的派生类了,就不能代替VIP出现。

         如果两个类从逻辑上就没有衍生的关系,就不应该有相互继承出现,见下图9-6:

图9-6 没有衍生关系的两个物体

如上图9-6所示,狗跟猫两种动物没有衍生关系,狗类(Dog)不能继承自猫类(Cat),猫类也不能继承自狗类,但是他们都可以同时继承自动物类(Animal)。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值