面向对象设计原则(转贴)

面向对象设计原则

面向对象设计原则
面向对象设计的基石是“开—闭”原则。
   “开一闭”原则讲的是:一个软件实体应当对扩展开放,对修改关闭。
    这个规则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。
    从另外一个角度讲,就是所谓的“对可变性封装原则”。“对可变性封装原则”意味着两点:
    1 .一种可变性不应当散落在代码的很多角落里,而应当被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承等级结构中的具体子类。
    2.一种可变性不应当与另一种可变性混合在一起。即类图的继承结构一般不应超过两层。
    做到“开—闭”原则不是一件容易的事,但是也有很多规律可循,这些规律同样也是设计原则,它们是实现开—闭原则的工具。


里氏代换原则
    里氏代换原则:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都换成o2时,程序P的行为没有变化,那么类型T2是T1的子类型。
    即如果一个软件实体使用的是基类的话那么也一定适用于子类。但反过来的代换不成立。
    如果有两个具体类A和B之间的关系违反了里氏代换原则,可以在以下两种重构方案中选择一种:
    1 .创建一个新的抽象类C,作为两个具体类的超类,将A和B共同的行为移动到C中,从而解决A和B行为不完全一致的问题。
    2 .从B到A的继承关系改写为委派关系。

依赖倒转原则
    依赖倒转原则讲的是:要依赖于抽象,不要依赖于具体。即针对接口编程,不要针对实现编程。针对接口编程的意思是,应当使用接口和抽象类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。不要针对实现编程的意思就是说,不应当使用具体类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。
    依赖倒转原则虽然强大,但却不易实现,因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以避免对具体类的直接引用,此原则的使用还会导致大量的类。维护这样的系统需要较好的面向对象的设计知识。
    此外,依赖倒转原则假定所有的具体类都是变化的,这也不总是正确的。有一些具体类可能是相当稳定、不会发生变化的,消费这个具体类实例的客户端完全可以依赖于这个具体类。

接口隔离原则
    接口隔离原则讲的是:使用多个专门的接口比使用单一的接口要好。从客户的角度来说:一个类对另外一个类的依赖性应当是建立在最小的接口上的。如果客户端只需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。提供接口意味着向客户端作出承诺,过多的承诺会给系统的维护造成不必要的负担。

合成、聚合复用原则
    合成、聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部份,新的对象通过向这些对象的委派达到复用已有功能的目的。这个原则有一个简短的描述:要尽量使用合成、聚合,尽量不要使用继承。
合成、聚合有如下好处:
新对象存取成分对象的唯一方法是通过成分对象的接口。

这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不到的。

这种复用可以在运行时间内动态进行,新对象可以动态的引用与成分对象类型相同的对象。
合成、聚合可以应用到任何环境中去,而继承只能应用到一些有限环境中去。
    导致错误的使用合成、聚合与继承的一个常见原因是错误的把“Has-a”关系当作“Is-a”关系。如果两个类是“Has-a”关系那么应使用合成、聚合,如果是“Is-a”关系那么可使用继承。

迪米特法则
    迪米特法则说的是一个对象应该对其它对象有尽可能少的了解。即只与你直接的朋友通信,不要跟陌生人说话。如果需要和陌生人通话,而你的朋友与陌生人是朋友,那么可以将你对陌生人的调用由你的朋友转发,使得某人只知道朋友,不知道陌生人。换言之,某人会认为他所调用的是朋友的方法。
以下条件称为朋友的条件:
     当前对象本身。
     以参量的形式传入到当前对象方法中的对象。
     当前对象的实例变量直接引用的对象。
     当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友。
     当前对象所创建的对象。
     任何一个对象,如果满足上面的条件之一,就是当前对象的朋友,否则就是陌生人。

迪米特法则的主要用意是控制信息的过载,在将其运用到系统设计中应注意以下几点:
    在类的划分上,应当创建有弱耦合的类。类之间的耦合越弱,就越有利于复用。
    在类的结构设计上,每一个类都应当尽量降低成员的访问权限。一个类不应当public自己的属性,而应当提供取值和赋值的方法让外界间接访问自己的属性。
    在类的设计上,只要有可能,一个类应当设计成不变类。
    在对其它对象的引用上,一个类对其它对象的引用应该降到最低。
 一个良好的面向对象设计需要遵循一些基本原则,如单一职责原则(SRP)、开放-封闭原则(OCP)、Liskov替换原则(LSP)、依赖倒置原则(DIP)、接口分离原则(ISP)等。

1、 单一职责原则(SRP)
描述:就一个类而言,应该仅有一个引起它变化的原因。
应用:在构造对象时,将对象的不同职责分离至两个或多个类中,确保引起该类变化的原因只有一个。
带来的好处:提高内聚、降低耦合。
个人观点:该原则可以有效降低耦合,减少对不必要资源的引用。但后果是造成源文件增多,给管理带来不便,所以在实际应用中,可以对经常使用或经常需要改动的模块应用该原则。

2、 开放-封闭原则(OCP)
描述:"对于扩展是开放的"(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,可以对模块进行扩展,使其具有满足改变的新行为。也就是说,我们可以改变模块的功能。"对于更改是封闭的"(Close for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。
应用:高级语言中的接口与虚拟类。
带来的好处:提高灵活性、可重用性、可维护性。
个人观点:OCP的关键是抽象,抽象的目的是创建一个固定却能够描述一组任意个可能行为的基类。而这一组可能的行为则表现为派生类。对于基类的更改是封闭的,所以它里边的方法一旦确定就不能更改(对接口里的方法进行更改将带来灾难性的后果)。模块通过抽象基类进行引用,对派生类的扩展并不影响整个模块,所以它是开放的。遵循OCP的代价也是昂贵的,创建正确的抽象是要花费开发时间和精力的,同时抽象也增加了软件设计的复杂性。因此有效的预知变化是OCP设计的要点,这需要我们进行适当的调查,提出正确的问题,并利用我们的经验和一般常识来做出判断。正确的做法是,只对程序中频繁变化的部分做出抽象,拒绝不成熟的抽象和抽象本身一样重要。

3、 Liskov替换原则(LSP)
描述:若对每个类型S的对象O1,都存在一个类型T的对象O2,使得在所有针对T编写的程序P中,用O1替换O2后,程序P行为功能不变,则S是T的子类型。
应用:在实现继承时,子类型(subtype)必须能替换掉它们的基类型(base type)。如果一个软件实体使用的是基类的话那么也一定适用于子类。但反过来的代换不成立。
个人观点: LSP是使OCP成为可能的主要原则之一,对LSP的违反将导致对OCP的违反,同时二者是OOD中抽象和多态的理论基础,在OOPL中表现为继承。在高级语言(JAVA、C#)中,只要我们严格按照接口和虚拟类的语法规范来做就能很好遵循此原则,另外我们还应该避免一些更微妙的违规情况。举个例子,正方形和矩形,矩形可以做为正方形的基类,因为正方形也是一种矩形,但对于正方形来说,setWidth()和setHeight()是冗余的,且容易引起错误,这样的设计就违反了LSP原则。如果有两个具体类A和B之间的关系违反了LSP,可以在以下两种重构方案中选择一种:1 .创建一个新的抽象类C,作为两个具体类的超类,将A和B共同的行为移动到C中,从而解决A和B行为不完全一致的问题。 2 .从B到A的继承关系改写为委派关系。

4、 依赖倒置原则(DIP)
描述:A .高层模块不应该依赖于低层模块。二者都应该依赖于抽象。B .抽象不应该依赖于细节。细节应该依赖于抽象。
应用:要依赖抽象,不要依赖于具体。即针对接口编程,不要针对实现编程。针对接口编程的意思是,应当使用接口和抽象类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。不要针对实现编程的意思就是说,不应当使用具体类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。
结论:DIP虽然强大,但却不易实现,因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以避免对具体类的直接引用,此原则的使用将导致大量的类文件。给维护带来不必要的麻烦。所以,正确的做法是只对程序中频繁变化的部分进行依赖倒置。

5、 接口隔离原则(ISP)
描述:不要强迫客户依赖于它们不用的方法。
应用:一个类对另外一个类的依赖性应当是建立在最小的接口上的。如果客户端只需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。提供接口意味着向客户端作出承诺,过多的承诺会给系统的维护造成不必要的负担。
结论:使用多个专门的接口比使用单一的接口要好。

遵循以上原则,可以使我们的软件更具灵活性,强壮性。但灵活是需要付出代价的,由多态带来的性能损失就是最明显的一个问题。所以我们需要权衡,需要做出选择,在灵活与性能之间做出选择。

追本溯源,促使我们使用这些原则的原因是为了满足需求的变更,于是需求分析就显得格外重要。然而不管怎么充分的需求分析都可能遭遇需求变更,于是预测变化就成了一个让人头痛的事。还是让我们来看看敏捷设计(XP)是怎么解决这些问题的:"敏捷开发人员不会对一个庞大的预先设计应用那些原则和模式,相反,这些原则和模式被应用在一次次的迭代中,力图使代码以及代码所表达的设计保持干净。"也就是说敏捷设计通过快速的迭代来刺激变化,让这些变化及早暴露,再根据变化进行相应改动。很明显这要比一次性完整设计轻松容易的多。

最后引用透明在书评中的一句话来结束这篇blog。"软件开发的全部艺术就是权衡:在简单与复杂之间权衡,在一种方案与另一种方案之间权衡。如果把每个问题、每个权衡的利弊都考虑得清清楚楚,恐怕开发一个应用程序的成本会高得惊人。所以,很多时候我们更依赖自己的审美眼光,用平静的心去设计一个赏心悦目的系统。"
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值