软件设计原则与模式

简介:

第九章 设计模式与原则

      软件设计模式(Design pattern)是一套被反复使用的代码设计经验总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。好的设计,成就好的作品。但在软件设计的过程中,若有一些设计原则(Design Principle)的约束,那我们的软件会重构得更好。设计模式和设计原则博大精深,需要我们长时间的实践和总结才能真正领悟到其真谛,本章首先以“观察者模式”为例,介绍设计模式在Windows Forms中的应用(其他常用设计模式略),之后详细介绍五大设计原则(简称Solid原则)。

9.1软件的设计模式

9.1.1观察者模式

      程序的运行意味着模块与模块之间、对象与对象之间不停地有数据交换,观察者模式强调的就是,当一个目标本身的状态发生改变时(或者满足某一条件),它会主动发出通知,通知对该变化感兴趣的其他对象,如果将通知者称为“Subject”(主体),将被通知者称为“Observer”(观察者),具体结构图如下:

 

 

图9-1 观察者模式中类关系图

如上图9-1所示,图中将主体和观察者的逻辑抽象出来两个接口,分别为:ISubject和IObserver,ISubject接口中包含一个通知观察者的NotifyObservers方法、一个添加观察者的AddObserver方法和一个RemoveObserver方法,IObserver接口中则只包含一个接受通知的Notify方法,ISubject和IObserver接口的关系为:一对多,一个主体可以通知多个观察者。

      具体代码实现如下:

  View Code

如上代码Code 9-1中所示,NO.1和NO.2处分别定义了ISubject和IObserver接口,接着定义了一个具体的主体类MySubject,该类实现了ISubject接口,在AddObserver、RemoveObserver分别将观察者加入或者移除集合_observers_list(NO.3和NO.4处),最后在NotifyObservers方法中,遍历_observers_list集合,将通知发送到每个观察者(NO.5处),注意我们可以在DoSomething方法中当满足某一条件时,通知观察者(NO.6处)。我们使用IObserver接口定义了两个具体的观察者MyObserver和YourObserver,在两者的Notify方法中分别按照自己的逻辑去处理通知信息(一个直接将msg打印出来,一个将msg以邮件形式发送给别人)(NO.7和NO.8处)。

      现在我们可以将MySubject类对象当作一个具体的主体,将MyObserver类对象和YourObserver类对象当做具体的观察者,那么代码中可以这样去使用:

  View Code

如上代码Code 9-2所示,我们向主体subject中添加两个观察者(NO.1和NO.2处),之后使用ISubject.NotifyObservers方法通知观察者(NO.3),另外,我们还可以使用MySubject.DoSomething方法去通知观察者(当某一条件满足时),两个观察者分别会做不同的处理,一个直接将“it's a test”字符串打印输出,而另一个则将字符串以邮件的形式发送给别人。

      注:Code 9-2中,我们不能使用ISubject接口去调用DoSomething方法,而必须先将ISubject类型转换成MySubject类型,因为DoSomething不属于ISubject接口。

      观察者模式中,整个流程见下图9-2:

 

 

图9-2 观察者模式中的运行流程

如上图9-2所示,在有些情况中,NO.2处会做一些筛选,换句话说,主体有可能根据条件通知部分观察者,NO.4处虚线框表示可选,如果主体关心观察者的处理结果,那么观察者就应该将自己的处理结果返回给主体。“观察者模式”是所有框架使用得最频繁的设计模式之一,原因很简单,“观察者模式”分隔开了框架代码和框架使用者编写的代码,它是“好莱坞原则”(Hollywood Principle,don't call us,we will call you)的具体实现手段,而“好莱坞原则”是所有框架都严格遵守的。

      Windows Forms框架中的“观察者模式”主要不是通过“接口-具体”这种方式去实现的,更多的是使用.NET中的“委托-事件”去实现,详见下一小节。

9.1.2Windows Forms中的观察者模式

      在Windows Forms框架中,可以说“观察者模式”无处不在,在第四章讲Winform程序结构时已经有所说明,比如控件处理Windows消息时,最终是以“事件”的形式去通知事件注册者的,那么这里的事件注册者就是观察者模式中的“观察者”,控件就是观察者模式中的“主体”。我们回忆一下第四章中有关System.Windows.Forms.Control类的代码(部分):

  View Code

如上代码Code 9-3所示,在Control类的WndProc窗口过程中的switch/case块中,会根据不同的Windows消息去激发不同的事件(NO.1和NO.2处),由于WndProc是一个虚方法,所有在任何一个Control的派生类中,均可以重写WndProc虚方法,处理Windows消息,然后以“事件”的形式去通知事件注册者。

      如果我们在Form1中注册了一个Button类对象btn1的Click事件,那么btn1就是观察者模式中的“主体”,Form1(的实例)就是观察者模式中的“观察者”,如下代码:

  View Code

如上图Code 9-4代码所示,我们在Form1的构造方法中注册了btn1的Click事件(NO.1处),那么btn1就是“主体”,Form1(的实例)就是“观察者”,当btn1需要处理Windows消息时,就会激发事件,通知Form1(的实例)。

      Windows Forms框架正是使用“观察者模式”实现了框架代码与框架使用者编写的代码相分离。

      注:我们可以认为,事件的发布者等于观察者模式中的“主体”(Subject),而事件的注册者等于观察者模式中的“观察者”,有关“事件编程”,请参考第六章。

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:

  View Code

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

 

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

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

  View Code

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

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

9.2.3开闭原则(OCP)

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

  View Code

如上代码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类可以这样定义:

  View Code

如上代码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类,代码如下:

  View Code

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

  View Code

如上代码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接口即可,如下代码:

  View Code

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

  View Code

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

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

 

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

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

9.2.5接口隔离原则(ISP)

      “一个类型不应该去实现它不需要的接口,换句话说,接口应该只包含同一类方法或属性等”,如果把所有的方法都放在一个接口中,那么实现了该接口的类型必须实现接口中的全部方法(即使不需要),同理,在一个已经很稳定的系统中,不应该再去修改已经存在的接口,因为这会影响到之前所有实现该接口的类型。现在如果需要新增加一种VIP顾客(SuperVIP),允许它修改数据库,我们可能这样去修改IData接口:

  View Code

如上代码Code 9-13所示,我们修改已经存在的IData接口,使其包含一个写数据库的Write方法(NO.1处),满足SuperVIP类的需要,这个方法看似可以,但是它要求我们修改其他已经实现了IData接口的类型,比如前面的VIP类,只要涉及到VIP类的更改,那么其他所有使用到了VIP类的地方都得重新测试,可以看出,这会影响整个已经存在的系统。正确的做法应该是,新增加一个接口IData2,将数据库的写入方法放在该接口中,让SuperVIP类实现该接口,代码如下:

  View Code

如上代码Code 9-14所示,我们定义了一个新的接口IData2(NO.1处),该接口包含一个Write方法,让SuperVIP类实现该接口,这样一来,整个过程不会影响已经存在的VIP类。

9.2.6依赖倒置原则(DIP)

      “高层模块不应该依赖于低层模块,高层模块和低层模块应该同时依赖于一个抽象层(接口层)”,本原则目的很明确,就是为了降低模块之间的耦合度,我们观察一下9.2.2小节示例代码中的VIP类和Logger类,很明显,VIP类直接依赖于Logger类,如果我们想换种方式记录日志的话(也就是改变记录日志的逻辑),必须得重新修改Logger类中的代码,现在如果让VIP类依赖于一个抽象接口ILog,其他所有记录日志的类型同时也依赖于ILog接口,那么整个系统就会更加灵活,

  View Code

如上代码Code 9-15所示,我们定义了一个日志接口ILog作为抽象层(NO.1处),之后定义了各种各样的低层日志模块(NO.2、NO.3和NO.4处),这些记录日志的类均依赖(实现)ILog这个抽象接口,之后我们在定义VIP类时,不再让它具体依赖于某个日志类,换句话说,不再让高层模块直接依赖低层模块,取而代之的是,让VIP类依赖于ILog这个抽象接口(NO.6处),我们在使用VIP类的时候,可以根据需要给它传递不同的日志类对象(也可以是除了示例代码中的三个以外自定义类型,只要实现了ILog接口),程序运行后,会将错误日志记录到相应位置(NO.7处)。我们可以这样使用VIP类:

  View Code

如上代码Code 9-16所示,NO.1处如果出现异常,错误日志会保存到文件,NO.2处如果出现异常,错误日志将会通过邮件发送给别人,NO.3处如果出现异常,VIP对象会自动把错误信息通知给别的模块。

     依赖倒置原则提倡模块与模块之间不应该有直接的依赖关系,见下图9-7:

 

图9-7 依赖倒置发生前后

如上图9-7所示,图中左边部分表示依赖倒置之前高层模块与低层模块之间的依赖关系,图中右边部分表示依赖倒置发生之后,高层模块与低层模块之间的依赖关系,很明显,依赖倒置发生后,高层模块不再直接受低层模块控制,高层模块与低层模块没有具体的对应关系,灵活性增加,耦合度降低。

 

图9-8 接力赛跑中的接力棒

如上图9-8所示,接力过程中前后两人没有具体对应关系。

       注:依赖倒置原则是每个框架都必须遵循的,框架不可能受框架的使用者控制,换句话说,框架作为“高层模块”,不应该依赖于框架使用者编写的代码(低层模块),而应该均依赖于一个抽象层,所以我们在使用框架编写代码时,大部分时候均以框架库作为基础,从已有的类型或者接口(抽象层)派生出新的类型。

9.3设计模式与设计原则对框架的意义

      “IT语境中的框架,特指为解决一个开放性问题而设计的具有一定约束性的支撑结构。在此结构上可以根据具体问题扩展、安插更多的组成部分,从而更迅速和方便地构建完整的解决问题的方案。”——摘自互联网

     上面是一段摘自互联网上描述“框架”的话,从这段话中我们了解到,首先,每个框架解决问题的范围是有限的,比如Windows Forms框架只会帮助我们完成Windows桌面应用程序的开发,这就是它的“约束性”,其次,框架本身解决不了什么特定的问题,它只给了解决特定问题的相关模块(或者组件)一个可插接、可组合的底子,这个底子为我们解决实际具体问题提供了支持,这就是框架的“支撑性”,见下图9-9:

 

图9-9 框架使用前后

如上图9-9所示,图中左边部分表示使用框架之前,整个系统均由开发者编写代码的结构图,我们可以看见,无论系统的“系统运行逻辑”还是“业务处理逻辑”均由开发者负责,开发者自己调用自己的代码,整个系统的运行流程由开发者控制;图中右边部分表示使用了框架之后,“系统运行逻辑”由框架接管了,开发者只需要把精力集中在“业务逻辑处理”之上(Windows Forms框架接管了消息循环、消息处理等,负责了整个Winform程序的运转),除此之外,还有一个非常大而且非常重要的改变:开发者不再(几乎不)自己调用自己的代码了,自己编写的代码均由框架调用,系统运行的控制权交给了框架。这就是所有框架所必须满足的“好莱坞原则”(Hollywood Principle,don't call us,we will call you),“好莱坞原则”跟“控制转换原则”(IoC,Inversion of Control)类似,参见前面章节,可以了解框架是怎样反过来控制程序的运行。

      我们在使用框架开发应用程序去解决实际具体的问题时,框架避免不了会与我们开发者编写的代码进行交互,这就会产生一个问题,那就是怎样去把握框架代码和框架使用者编写代码两者之间的关联性,也就是我们常说的“高内聚,低耦合”。“高内聚,低耦合”在框架中要求更高,因为框架的使用人群和范围比一般普通系统更大更广泛,优秀的框架要想使用寿命更长口碑更好,就要求框架能在使用后期能够更容易升级、更方便扩展新的功能来满足使用者的各种需要,而这些大部分取决于框架最开始的设计好坏,正确地使用各种“设计模式”以及严格地遵守各种“设计原则”是决定框架后期能否应付各种变更、升级扩展的重要因素。

作者:周见智 
出处:http://www.cnblogs.com/xiaozhi_5638/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

分类:  架构设计
标签:  架构设计

本文转自周见智博客博客园博客,原文链接:http://www.cnblogs.com/xiaozhi_5638/p/3610706.html,如需转载请自行联系原作者
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
|
程序员
编程原则和模式
编程原则和模式
|
6月前
|
存储 安全 算法
系统设计的端到端原则
系统设计的端到端原则
33 0
|
10月前
|
数据库
软件设计原则
软件设计原则
|
10月前
|
设计模式 算法
软件设计的原则
软件设计的原则
50 0
|
10月前
|
设计模式 Java uml
你的职责链模式符合五大原则吗?-系统学习九
工作之余对于用到的设计模式进行总结再梳理,发现职责链模式的妙处以及五大原则的指导下更能发挥职责链模式的优势于是乎便有了这篇博文的诞生
|
11月前
|
设计模式
软件设计原则有哪些(下)
软件设计原则有哪些(下)
56 0
|
11月前
|
设计模式 安全 Java
软件设计原则有哪些(上)
软件设计原则有哪些(上)
89 0
|
设计模式 测试技术 程序员
代码的简单设计五原则
代码的简单设计五原则
33033 1
|
消息中间件 架构师 Java
软件架构的23个基本原则
软件架构的23个基本原则
|
消息中间件 存储 架构师
软件架构的 23 个基本原则
软件架构的 23 个基本原则
252 0
软件架构的 23 个基本原则