软件设计应遵循的原则

Delicia ·
更新时间:2024-11-11
· 981 次阅读

一、单一职责原则(SRP)

一个类而言,应该仅有一个引起它变化的原因,如果你能想到多于一个的动机去改变一个类,那么这个类具有多于一个的指责.应该把多于的指责分离出去,分别再创建一些类来完成每一个指责.

二、开闭原则(OCP)

钻研OO设计模式有一段时间了,可是天生愚笨,总是不得真谛,于是想是不是该跳出来仔细的想一想了呢?为什么需要设计模式?GoF的23设计模式的设计原则是什么呢?在查阅了一些资料后,仿佛有了一些感觉,其实设计模式的原则是OOD的原则,或者说设计模式是为了达到OOD的远景而提出的,所以正真的想掌握OO的精髓,那么学习设计模式是好的途径,而想真正掌握设计模式的精髓,那麽必须好好的理解一下OOD的设计原则,这篇文章关注的只是其中的一个原则--OCP。下面通过引用CSDN上Health King的专栏的一篇我认为比较好的关于OCP原则的文章开始我们的认识OCP之旅吧!

     原文链接:http://blog.csdn.net/kxy/archive/2005/06/27/405013.aspx

     在继续《设计模式精解》这本书之前,我们来学习些OOD的一些设计原则。这些原则在提高一个系统可维护性的同时,提高这个系统的可复用性他们是一些指导原则,依照这些原则设计,我们可以有效的提高系统的复用性,同时提高系统的可维护性。

     这些OOD原则的一个基石是“开-闭原则”(Open-Closed Principle OCP)。这个原则早是由Bertrand Meyer提出,英文的原文是:Software entities should be open for extension,but closed for modification。意思是说,一个软件实体应当对扩展开放,对修改关闭。也是说,我们在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,换句话说是,应当可以在不必修改源代码的情况下改变这个模块的行为。

     满足OCP的设计给系统带来两个无可比拟的优越性:      通过扩展已有的软件系统,可以提供新的行为以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性。      已有的软件模块,特别是重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。

     具有这两个优点的软件系统是一个高层次上实现了复用的系统,也是一个易于维护的系统。那么,我们如何才能做到这个原则呢?不能修改而可以扩展,这个看起来是自相矛盾的。其实这个是可以做到的,按面向对象的说法,这个是不允许更改系统的抽象层,而允许扩展的是系统的实现层。

解决问题的关键在:抽象化。我们让模块依赖于一个固定的抽象体,这样它是不可以修改的;同时,通过这个抽象体派生,我们可以扩展此模块的行为功能。如此,这样设计的程序只通过增加代码来变化而不是通过更改现有代码来变化,前面提到的修改的副作用没有了。

   “开-闭”原则如果从另外一个角度讲述,是所谓的“对可变性封装原则”(Principle of Encapsulation of Variation, EVP)。讲的是找到一个系统的可变因素,将之封装起来。在我们考虑一个系统的时候,我们不要把关注的焦点放在什么会导致设计发生变化上,而是考虑允许什么发生变化而不让这一变化导致重新设计。也是说,我们要积极的面对变化,积极的包容变化,而不是逃避。

     [SHALL01]将这一思想用一句话总结为:“找到一个系统的可变因素,将它封装起来”,并将它命名为“对可变性的封装原则”。

    “对可变性的封装原则”意味者两点:      一种可变性应当被封装到一个对象里面,而不应当散落到代码的很多角落里面。同一种可变性的不同表象意味着同一个继承等级结构中的 具体子类。继承应当被看做是封装变化的方法,而不应当是被认为从一般的对象生成特殊的对象的方法(继承经常被滥用)。      一种可变性不应当与另外一种可变性混合在一起。从具体的类图来看,如果继承结构超过了两层,那么意味着将两种不同的可变性混合在了一起。

    “对可变性的封装原则”从工程的角度说明了如何实现OCP。如果按照这个原则来设计,那么系统应当是遵守OCP的。但是现实往往是残酷的,我们不可能的遵守OCP,但是我们要向这个目标来靠近。设计者要对设计的模块对何种变化封闭做出选择。

     好了,上面是引用的全文了。那麽在实际设计和开发之中,我们该如何重构我们的设计和代码呢?      答案是:抽象(Astraction)、多态(Polymorphism)、继承(Inheritance)、接口(Interface)。利用这些可以让我们去实践OCP了,这样会让我们的设计符合OCP,符合该法则便意味着高等级的复用性(Reusability)和可维护性(Maintainability)。当然我们也有很多的设计模式可以利用来很优美的解决如何封装变化等问题,但是不要忘了,设计模式的基础也是抽象(Astraction)、多态(Polymorphism)、继承(Inheritance)、接口(Interface)啊。还是从简单开始吧,千里之行,始于足下...

     考虑下面某个类的方法:但是现在每当计价策略发生改变,我们必须修改Part 的每个子类!      一个更好的思路是采用一个PricePolicy类,通过对其进行继承以提供不同的计价策略,那麽这里是在运用设计模式里面的策略模式了,解决起来算是很完美了:     看起来我们所做的是将问题推迟到另一个类中,将“变化”封装在PricePolicy类里面。但是使用该解决方案,我们可通过改变Part对象,在运行期间动态地来设定计价的策略。另一个解决方案是使每个ConcretePart从数据库或属性文件中获取其当前的价格,这样相当于把“变化”封装在了属性文件里面了。

    public double totalPrice(Part[] parts) {                double total = 0.0;                for(int i = 0;i                   total += parts[i].getPrice();              }                return total;          }    

  以上函数的工作是在制订的部件数组中计算各个部件价格的总和。若Part是一个基类或接口且使用了多态,则该类可很容易地来适应新类型的部件,而不必对其进行修改。其将符合OCP

     但是在计算总价格时,若财务部颁布主板和内存应使用额外费用,则将如何去做。下列的代码是如何来做的呢?这符合OCP吗?

    public double totalPrice(Part[] parts) {                double total = 0.0;                for(int i = 0;i                   if(parts[i] instanceof Motherboard)                          total += (1.45*parts[i].getPrice());                      else if(parts[i] instanceof Memory)                          total += (1.27*parts[i].getPrice());                      else                           total += parts[i].getPrice();                }                return total;          }  

    当每次财务部提出新的计价策略,我们都不得不要修改totalPrice()方法!这并非“对更改是封闭的”。显然,策略的变更便意味着我们不得不要在一些地方修改代码的,因此不符合OCP,那麽我们该如何去做呢?

    为了使用我们第一个版本的totalPrice(),我们可以将计价策略合并到Part的getPrice()方法中。     这里是Part和ConcretePart类的示例: Java代码  收藏代码

    public class Part {                 private double basePrice;                 public void setPrice(double price) {                     basePrice = price;                 }                 public double getPrice() {                     return basePrice;                 }          }          public class Motherboard extends Part {                 public double getPrice() {                     return 1.45*basePrice;                 }          }          public class Memory extends Part {                 public double getPrice() {                     return 1.27*basePrice;                 }          }    

     但是现在每当计价策略发生改变,我们必须修改Part 的每个子类!        一个更好的思路是采用一个PricePolicy类,通过对其进行继承以提供不同的计价策略,那麽这里是在运用设计模式里面的策略模式了,解决起来算是很完美了:     public class Part {                 private PricePolicy pricePolicy;                 public void setPricePolicy(PricePolicy policy) {                     pricePolicy = policy;                 }                 public void setPrice(double price) {                     pricePolicy.setPrice(price);                 }                 public double getPrice() {                     return pricePolicy.getPrice();                 }          }          public class PricePolicy {                 private double basePrice;                 public void setPrice(double price) {                     basePrice = price;                 }                 public double getPrice() {                     return basePrice;                 }          }             public class SalePrice extends PricePolicy {                 private double discount;                 public void setDiscount(double discount) {                     this.discount = discount;                 }                 public double getPrice() {                     return basePrice*discount;                 }             }        

看起来我们所做的是将问题推迟到另一个类中,将“变化”封装在PricePolicy类里面。但是使用该解决方案,我们可通过改变Part对象,在运行期间动态地来设定计价的策略。另一个解决方案是使每个ConcretePart从数据库或属性文件中获取其当前的价格,这样相当于把“变化”封装在了属性文件里面了。

三、里氏代换原则(LSP)

(1) 由Barbar Liskov(芭芭拉.里氏)提出,是继承复用的基石。 (2) 严格表达:如果每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换称o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型.     换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别.只有衍生类可以替换基类,软件单位的功能才能不受影响,基类才能真正被复用,而衍生类也能够在基类的基础上增加新功能。 (3) 反过来的代换不成立 (4) <墨子.小取>中说:"白马,马也; 乘白马,乘马也.骊马(黑马),马也;乘骊马,乘马也." (5) 该类西方的例程为:正方形是否是长方形的子类(答案是"否")。类似的还有椭圆和圆的关系。 (6) 应当尽量从抽象类继承,而不从具体类继承,一般而言,如果有两个具体类A,B有继承关系,那么一个简单的修改方案是建立一个抽象类C,然后让类A和B成为抽象类C的子类.即如果有一个由继承关系形成的登记结构的话,那么在等级结构的树形图上面所有的树叶节点都应当是具体类;而所有的树枝节点都应当是抽象类或者接口. (7) "基于契约设计(Design By Constract),简称DBC"这项技术对LISKOV代换原则提供了支持.该项技术Bertrand Meyer伯特兰做过详细的介绍: 使用DBC,类的编写者显式地规定针对该类的契约.客户代码的编写者可以通过该契约获悉可以依赖的行为方式.契约是通过每个方法声明的前置条件(preconditions)和后置条件(postconditions)来指定的.要使一个方法得以执行,前置条件必须为真.执行完毕后,该方法要保证后置条件为真.是说,在重新声明派生类中的例程(routine)时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件.

    一个软件实体如果使用的是一个基类的话那么一定适用于其子类,而且它察觉不出基类对象和子类对象的区别。也是说,在软件里面,把基类都替换成它的子类,程序的行为没有变化。

LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。    下面,我们从代码重构的角度来对LSP进行理解。LSP讲的是基类和子类的关系。只有当这种关系存在时,里氏代换关系才存在。如果两个具体的类A,B之间的关系违反了LSP的设计,(假设是从B到A的继承关系)那么根据具体的情况可以在下面的两种重构方案中选择一种。

    创建一个新的抽象类C,作为两个具体类的超类,将A,B的共同行为移动到C中来解决问题。      从B到A的继承关系改为委派关系。      为了说明,我们先用第一种方法来看一个例子,第二种办法在另外一个原则中说明。我们看那个的长方形和正方形的例子。对于长方形的类,如果它的长宽相等,那么它是一个正方形,因此,长方形类的对象中有一些正方形的对象。对于一个正方形的类,它的方法有个setSide和getSide,它不是长方形的子类,和长方形也不会符合LSP。

那么,如果让正方形当做是长方形的子类,会出现什么情况呢?我们让正方形从长方形继承,然后在它的内部设置width等于height,这样,只要width或者height被赋值,那么width和height会被同时赋值,这样保证了正方形类中,width和height总是相等的.现在我们假设有个客户类,其中有个方法,规则是这样的,测试传入的长方形的宽度是否大于高度,如果满足停止下来,否则增加宽度的值。现在我们来看,如果传入的是基类长方形,这个运行的很好。根据LSP,我们把基类替换成它的子类,结果应该也是一样的,但是因为正方形类的width和height会同时赋值,这个方法没有结束的时候,条件总是不满足,也是说,替换成子类后,程序的行为发生了变化,它不满足LSP。

那么我们用第一种方案进行重构,我们构造一个抽象的四边形类,把长方形和正方形共同的行为放到这个四边形类里面,让长方形和正方形都是它的子类,问题OK了。对于长方形和正方形,取width和height是它们共同的行为,但是给width和height赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。上面的例子中那个方法只会适用于不同的子类,LSP也不会被破坏。

在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这个只是一个一般性的指导原则,使用的时候还要具体情况具体分析。

四、依赖倒致原则(DIP)

要求客户端依赖于抽象耦合. (1) 表述:抽象不应当依赖于细节,细节应当依赖于抽象.(Program to an interface, not an implementaction)

(2) 表述二:针对接口编程的意思是说,应当使用接口和抽象类进行变量的类型声明,参量的类型声明,方法的返还类型声明,以及数据类型的转换等.不要针对实现编程的意思是说,不应当使用具体类进行变量的类型声明,参量类型声明,方法的返还类型声明,以及数据类型的转换等.    要保证做到这一点,一个具体的类应等只实现接口和抽象类中声明过的方法,而不应当给出多余的方法.    只要一个被引用的对象存在抽象类型,应当在任何引用此对象的地方使用抽象类型,包括参量的类型声明,方法返还类型的声明,属性变量的类型声明等.

(3) 接口与抽象的区别在于抽象类可以提供某些方法的部分实现,而接口则不可以,这也大概是抽象类的优点.如果向一个抽象类加入一个新的具体方法,那么所有的子类型一下子都得到得到了这个新的具体方法,而接口做不到这一点.如果向一个接口加入了一个新的方法的话,所有实现这个接口的类全部不能通过编译了,因为它们都没有实现这个新声明的方法.这显然是接口的一个缺点.

(4) 一个抽象类的实现只能由这个抽象类的子类给出,也是说,这个实现处在抽象类所定义出的继承的登记结构中,而由于一般语言都限制一个类只能从多一个超类继承,因此将抽象作为类型定义工具的效能大打折扣.    反过来,看接口,会发现任何一个实现了一个接口所规定的方法的类都可以具有这个接口的类型,而一个类可以实现任意多个接口.

(5) 从代码重构的角度上讲,将一个单独的具体类重构成一个接口的实现是很容易的,只需要声明一个接口,并将重要的方法添加到接口声明中,然后在具体类定义语句中加上保留字以继承于该接口行了.    而作为一个已有的具体类添加一个抽象类作为抽象类型不那么容易,因为这个具体类有可能已经有一个超类.这样一来,这个新定义的抽象类只好继续向上移动,变成这个超类的超类,如此循环,后这个新的抽象类必定处于整个类型等级结构的上端,从而使登记结构中的所有成员都会受到影响.

(6) 接口是定义混合类型的理想工具,所为混合类型,是在一个类的主类型之外的次要类型.一个混合类型表明一个类不仅仅具有某个主类型的行为,而且具有其他的次要行为.

(7) 联合使用接口和抽象类:    由于抽象类具有提供缺省实现的优点,而接口具有其他所有优点,所以联合使用两者是一个很好的选择.    首先,声明类型的工作仍然接口承担的,但是同时给出的还有一个抽象类,为这个接口给出一个缺省实现.其他同属于这个抽象类型的具体类可以选择实现这个接口,也可以选择继承自这个抽象类.如果一个具体类直接实现这个接口的话,它必须自行实现所有的接口;相反,如果它继承自抽象类的话,它可以省去一些不必要的的方法,因为它可以从抽象类中自动得到这些方法的缺省实现;如果需要向接口加入一个新的方法的话,那么只要同时向这个抽象类加入这个方法的一个具体实现可以了,因为所有继承自这个抽象类的子类都会从这个抽象类得到这个具体方法.这其实是缺省适配器模式(Defaule Adapter).

(8) 什么是高层策略呢?它是应用背后的抽象,是那些不随具体细节的改变而改变的真理. 它是系统内部的系统(隐喻).

五、接口隔离原则(ISP)

(1) 一个类对另外一个类的依赖是建立在小的接口上。 (2) 使用多个专门的接口比使用单一的总接口要好.根据客户需要的不同,而为不同的客户端提供不同的服务是一种应当得到鼓励的做法.像"看人下菜碟"一样,要看客人是谁,再提供不同档次的饭菜. (3) 胖接口会导致他们的客户程序之间产生不正常的并且有害的耦合关系.当一个客户程序要求该胖接口进行一个改动时,会影响到所有其他的客户程序.因此客户程序应该仅仅依赖他们实际需要调用的方法.     六、合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)

在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过这些向对象的委派达到复用已有功能的目的.这个设计原则有另一个简短的表述:要尽量使用合成/聚合,尽量不要使用继承.

七、迪米特法则(Law of Demeter LoD)

又叫做少只是原则(Least Knowledge Principle,LKP),是说,一个对象应当对其他对象有尽可能少的了了解. 迪米特法则初是用来作为面向对象的系统设计风格的一种法则,与1987年秋天由Ian Holland在美国东北大学为一个叫做迪米特(Demeter)的项目设计提出的,因此叫做迪米特法则[LIEB89][LIEB86].这条法则实际上是很多系统,比如火星登陆软件系统,木星的欧罗巴卫星轨道飞船的软件系统的指导设计原则. 没有任何一个其他的OO设计原则象迪米特法则这样有如此之多的表述方式,如下几种: (1) 只与你直接的朋友们通信(Only talk to your immediate friends) (2) 不要跟"陌生人"说话(Don't talk to strangers) (3) 每一个软件单位对其他的单位都只有少的只是,而且局限于那些本单位密切相关的软件单位. 是说,如果两个类不必彼此直接通信,那么这两个类不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。



软件设计 软件

需要 登录 后方可回复, 如果你还没有账号请 注册新账号