Java设计模式之面向对象设计原则

面向对象设计原则

面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则,但并不是强制性的。
面向对象设计原则也是学习设计模式的基础,每一个设计模式都符合某一个或多个面向对象设计原则,面向对象设计原则是用于评价一个设计模式的使用效果的重要指标之一。
最常见的7个面向对象设计原则如下:

设计原则名称定义
单一职责原则(Single Responsibility Principle,SRP)一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中
开闭原则(Open-Closed Principle,OCP)软件实体应当对扩展开放,对修改关闭
里氏代换原则(Liskov Substitution Principle,LSP)所有引用基类的地方必须能透明地使用其子类的对象
依赖倒转原则(Dependence Inversion Principle,SRP)高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象
接口隔离原则(Interface Segregation Principle,ISP)客户端不应该依赖那些它不需要的接口
合成复用原则(Composite Reuse Principle,CRP)优先使用对象组合,而不是通过继承来达到复用的目的
迪米特法则(Law of Demeter,LoD)每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位

1、单一职责原则

单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。
定义如下:

单一职责原则: 一个对象应该只包含单一的职责,并且该职责被完整的封装在一个类中。
Single Responsibility Principle(SRP): Every object should have a single responsibility,
and that responsibility should be entirely encapsulated by the class.

另一种定义方式:就一个类而言,应该仅有一个引起它变化的原因。
(There should never be more than one reason for a class to change.)

在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,相当于将这些职责耦合在一起,当其中一个职责变化时可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。

  • 单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
  • 单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。

下面通过一个简单实例来进一步分析单一职责原则:

某软件公司开发人员针对CRM(Customer Relationship Management,客户关系管理)系统中的客户信息图形统计模块
提出了如图所示的初始设计方案。
 在这里插入图片描述
在CustomerDataChart类的方法中,getConnection()方法用于连接数据库,findCustomers()用于查询所有的客户信息,
createChart()用于创建图表,displayChart()用于显示图表。
现使用单一职责原则对其进行重构。

在图中,CustomerDataChart类承担了太多的职责,既包含于数据库相关的方法,又包含于图表生成和显示相关的方法。如果在其他类中也需要连接数据库或者使用findCustomers()方法查询客户信息,则难以实现代码的重用。无论是修改数据库连接方法还是修改图表显示方式都需要修改该类,它拥有不止一个引起它变化的原因,违背了单一职责原则。因此需要对该类进行拆分,使其满足单一职责原则,CustomerDataChart类可拆分为以下3个类。
(1)DBUtil:负责连接数据库,包含数据库连接 方法getConnection()。
(2)CustomerDAO:负责操作数据库中的Customer表,包含对Customer表的增删改查等方法,如findCustomers()。
(3)CustomerDataChart:负责图表的生成和显示,包含createChart()和displayChart()方法。

2、开闭原则

开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则。
定义如下:

开闭原则: 软件实体应当对扩展开放,对修改关闭。
Open-Closed Principle(OCP): Software entities should be open for extension,
but closed for modification.

在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。开闭原则就是指软件实体应尽量在不修改原有代码的情况下进行扩展。

任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无需修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。

  • 为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在很多面向编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。

3、里氏代换原则

定义如下:

里氏代换原则: 所有引用基类的地方必须能透明地使用其子类的对象。
Liskov Substitution Principle(LSP): Functions that use pointers or references to base classes
must be able to use objects of derived classes without knowing it.

里氏代换原则表明:在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立。例如我喜欢动物,那么我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢所有的动物。

里氏代换原则是实现开闭原则的重要方式之一,由于在使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类
类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

  • 在运用里氏代换原则时应该将父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,在运行时子类实例替换父类实例,可以很方便地扩展系统的功能,无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。

4、依赖倒转原则

如果说开闭原则是面向对象设计的目标,那么依赖倒转原则是面向对象设计的主要实现机制之一,它是系统抽象化的具体实现。
定义如下:

依赖倒转原则: 高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
Dependence Inversion Principle(DIP): High level modules should not depend upon low level modules,
both should depend upon abstractions. Abstractions should not depend upon details,details should
depend upon abstractions.

简单来说,依赖倒转原则要求针对接口编程,不要针对实现编程。(Program to an interface,not an implementation.)

  • 依赖倒转原则要求在程序代码中传递参数时或在关联关系中尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。

在实现依赖倒转原则时需要针对抽象层进行编程,而将具体类的对象通过依赖注入(Dependence Injection,DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时采用抽象的形式来注入所依赖的对象。常用的注入方式有3种,分别是构造注入、设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。

在大多数情况下,开闭原则、里氏代换原则和依赖倒转原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时角度不同而已。

5、接口隔离原则

定义如下:

接口隔离原则: 客户端不应该依赖那些它不需要的接口。
Interface Segregation Principle(ISP): Client should not be forced to depend upon interfaces
that they do not use.

根据接口隔离原则,当一个接口太大时需要将它分割成一些更细小的接口,使用该接口的客户端仅需要知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的“接口”,有严格的定义和接口,比如Java和C#语言中的interface。对于这两种不同的含义,ISP的表达方式以及含义也有所不同。
(1)当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫“角色隔离原则”。
(2)如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。在面向对象编程语言中,实现一个接口需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。

  • 在使用接口隔离原则时需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中的接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,在接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些他们不用的方法。

6、合成复用原则

又称组合/聚合复用原则(Compositoin/Aggregate Reuse Principle,CARP),定义如下:

合成复用原则: 优先使用对象组合,而不是通过继承来达到复用的目的。
Composite Reuse Principle(CRP): Favor composition of objects over inheritance as a reuse mechanism.

合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用功能的目的。简而言之,在复用时要尽量使用组合/聚合关系(关联关系),少用继承。

  • 在面向对象设计中可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的某些内部细节对子类来说使可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。

由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。

一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A"的关系可以使用继承。”Is-A“是严格的分类学意义上的定义,意思是一个类是另一个类的”一种“;而”Has-A“则不同,它表示某一个角色具有某一项责任。

7、迪米特法则

迪米特法则又称最少知识原则(Least Knowledge Principle,LKP),定义如下:

迪米特法则: 每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
Law of Demeter(LoD): Each unit should have only limited knowledge about other units:
only units “closely” related to the the current unit.

迪米特法则要求一个软件实体应当尽可能少地与其他实体发生相互作用。如果一个系统符合迪米特法则,那么当其中的某一个模块发生修改时就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。应用迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。

迪米特法则还有几种定义形式,包括不要和“陌生人”说话(Don’t talk to strangers.)、只与你的直接朋友通信(Talk only to your immediate friends.)等。在迪米特法则中,对于一个对象,其朋友包括以下几类:
(1)当前对象本身(this)。
(2)以参数形式传入到当前对象方法中的对象。
(3)当前对象的成员对象。
(4)如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友。
(5)当前对象所创建的对象。
任何一个对象如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与“陌生人”发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。

迪米特法则要求在设计系统时应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中一个对象需要调用另一个对象的方法,可以通过“第三者”转发这个调用。简而言之,就是通过引入一个合理的“第三者”来降低现有对象之间的耦合度。

  • 在迪米特法则运用到系统设计中时要注意下面几点:在类的划分上应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改不会对惯量的类造成太大影响;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只有有可能,一个类型应该设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

小结

  1. 在软件开发中使用面向对象设计原则可以提高软件的可维护性和可复用性,以便设计出兼具良好的可维护性和可复用性的软件系统,实现可维护性复用的目标。
  2. 单一职责原则要求在软件系统中一个对象只包含单一的职责,并且该职责被完整地封装在一个类中。
  3. 开闭原则要求软件实体应当扩展开放,对修改关闭。
  4. 里氏代换原则可以通俗表述为在软件中所有引用基类的地方必须能透明地使用其子类的对象。
  5. 依赖倒转原则要求高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
  6. 接口隔离原则要求客户端不应该依赖那些它不需要的接口。
  7. 合成复用原则要求优先使用对象组合,而不是通过继承来达到复用的目的。
  8. 迪米特法则要求每一个软件单位对其他单位都只有最少的知识,而且局限于那么与本单位密切相关的软件单位。

以上文字,大量摘抄自《Java设计模式》一书,由刘伟老师编著,故本文应当列入转载一类,有兴趣的朋友可以直接阅读原书。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值