设计模式学习第一节:面向对象设计原则

1.1 面向对象设计原则概述

   软件的可维护性(Maintainability)和可复用性(Reusability)是两个非常重要的用于衡量软件质量的属性,软件的可维护性是指软件能够被理解、改正、适应以及扩展的难易程度,软件的可复用性是指软件被重复使用的难易程度。‘
面向对象设计的目标之一在于支持可维护性复用,一方面需要实现设计方案或者源代码的复用,另一方面要确保系统能够易于扩展和修改,具有良好的可维护性。面向对象设计原则为支持可维护性复用而诞生,这些原则被运用于许多的设计模式中,它们是从许多设计方案中总结出来的指导性原则,但并不是强制性的。
面向对象设计原则是学习设计模式的基础,每个设计模式都符合一个或多个面向对象设计原则,面向对象设计原则是用于评价一个设计模式的使用效果的重要指标之一。通过在软件开发中使用这些原则可以提高软件的可复用性和可维护性,以便设计出兼具良好的可维护性和可复用性的软件系统,实现可维护性复用的目标。

下表为7个常用的面向对象设计原则

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

1.2 单一职责原则

  单一职责原则是最简单的面向对象设计原则,用于控制类的粒度大小

单一职责原则的定义:
    单一职责原则:一个对象应该只包含单一的职责,并且该职责被完整的封装在一个类中
    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系统中的客户信息图形统计模块提出了如下所示的初始设计方案:
在这里插入图片描述

  在CustomerDataChart类中getConnection()方法用于连接数据库,findCustomers()用于查询所有的客户信息,createChart()用于创建图表,showChart()用于显示图表。
  在初始设计方案中CustomerDataChart类承担了太多的职责,既包含了与数据库相关的方法,又包含图表的创建与显示。如果其他类中也需要连接数据库或者查询用户,则难以实现代码的重用。无论时连接数据库还是操作图表都需要修改类,它拥有不止一个引起它变化的原因,违背了单一职责原则。因此需要对该类进行拆分,使其满足单一职责原则,CustomerDataChart类可以拆分为以下3个类:
  ①DBUtil:负责连接数据库,包含getConnection()方法
  ②CustomerDao:负责操作Customer表,包含增删改查,例如findCustomers()
  ③CustomersDataChart:用于图表的生成与显示,包含createChart()和showChart()方法
使用单一职责原则重构后的结构如下:
在这里插入图片描述

1.3 开闭原则

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

开闭原则:软件实体应当对扩展开发,对修改关闭
Open-Closed Principle(OPC): Software entities should be open for extension, but closed for modification
开闭原则就是指软件实体应该尽量在不修改原有代码的情况下进行扩展抽象

  抽象化是开闭原则的关键,在java,c#多数编程语言都可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,在通过具体的类进行扩展。如果需要修改系统的行为,无需对抽象层进行任何的改动,只需要增加新的具体类来实现新的业务需求即可,实现在不修改已有代码的基础上增加功能,达到开闭原则的要求。

1.4 里氏替代原则

  里氏替代原则严格描述如下:如果对每一个类型为S的对象o1都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都替换为o2时,程序P的行为不会发生任何变化,那么类型S则为类型T的子类型。

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

  里氏替代原则表明在软件中将一个基类对象替换为它的子类对象,程序不会产生任何错误和异常,反之则不成立。由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型对对象进行定义,而在运行时再确定其子类对象类型,用子类对象替换父类对象。在使用里氏替代原则时应该尽量将父类设计为抽象类或者接口,让子类继承父类或者实现父类接口,并实现在父类中声明的方法,在运行时子类对象替换父类对象,可以方便的扩展系统功能而不修改原有子类代码。

1.5 依赖倒转原则

  依赖倒转原则时面向对象设计的主要实现机制之一,它是系统抽象化的具体实现。

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

  依赖倒转原则要求针对接口编程,而不要针对实现编程。为了确保该原则的应用,一个具体类应该只实现接口或者抽象类中声明过的方法,而不要给出多余的方法,否则无法调用到在子类中新增的方法。
  在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在抽象文件中,这样如果系统发生变化,只需要对抽象层进行扩展,并修改配置文件,而无需修改系统原有的代码,在不修改的情况下来扩展系统功能,满足开闭原则的要求。
  在实现依赖倒转原则时需要针对抽象层编程,而将具体类的对象通过依赖注入的方式注入到其它对象中,依赖注入是指当一个对象要与其它对象发生依赖关系时采用抽象的形式来注入所依赖的对象。常用的注入方式有三种:
  构造注入:通过构造函数来传入具体类的对象
  设值注入:通过Setter方法来传入具体类的对象
  接口注入:通过在接口中声明的业务方法来传入具体类的对象,这些方法在定义时使用的是抽象类型,在运行是再传入具体 类型的对象,有子类来覆盖父类对象

案例:
  现有一需求要将存储在TXT文件或者Excel文件中的客户信息存储到数据库中,因为要进行格式转换,所以在客户数据操作类CustomerDao中调用数据格式转换类的方法来实现数据格式转化,初始设计方案如下:
在这里插入图片描述

  发现问题:
  由于转化的文件格式不同,需要经常修改CustomerDao中的源代码,而且在引入新的数据格式转化时也需要修改CustomerDao源码,违反了开闭原则,现需要重构方案
  解决方案:
  引入抽象数据转换类DataConvertor,CustomerDao针对抽象类DataConvertor编程,将具体数据转换类名存储在配置文件中,符合依赖倒转原则。根据里氏替代原则,程序运行时具体数据转换类对象将替换DataConvertor类型的对象,程序不会有任何异常。在更换具体数据转换类时无需修改源代码,只需要修改配置文件即可;新增转换数据类型时,只要将新增的数据转换类作为DataConvertor子类并修改配置文件即可,无需对源码做任何改动,符合开闭原则。重构后结构如下:
在这里插入图片描述
大多数情况下,开闭原则、里氏替代原则、依赖倒转原则会同时出现,开闭原则是目标,里氏替代原则是基础,依赖倒转原则是手段,它们相辅相成,互相补充。

1.6 接口隔离原则

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

根据接口隔离原则,每个接口应该承担一种相对独立的角色,该干的事都干完,不该干的事都不干。这里的"接口"有两种理解:
  1.当把"接口"理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的接口,此时这个原则可以叫"角色隔离原则"。
  2.如果把"接口"理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为。在面向对象编程语言中,实现一个接口需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据职责不同分别放在不同的小接口中,以确保每个接口使用起来都很方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端所需的方法即可,这种机制也称为"定制服务",即为不同的客户端提供宽窄不同的接口。
  案例:
  某客户数据显示模块设计了接口CustomerDataDisplay,其中方法readData()用于从文件中读取数据,方法transformToXML()用于将数据转换为XML格式,方法createChar()用于创建图表,方法createChart()用于显示图表,方法createReport()用于创建文字报表,方法dispalyChart()用于显示文字报表。设计方案如下图所示:
在这里插入图片描述
  在实际使用中发现该接口很不灵活,每次调用要实现的方法太多,有些方法不需要使用也不得不进行声明进行空实现。例如现在想创建一个图表并显示,但由于实现了接口,不得不把创建报表,数据转换等接口都声明一遍。现基于接口隔离原则对该接口进行重构。
在这里插入图片描述
  重构后的每个接口都承担某一单一角色,每个接口中只包含一个客户端所需的方法。
  注意:在使用接口隔离原则时要注意控制接口的粒度,接口太小容易导致系统中的接口泛滥,不利于维护;接口太大则将违背接口分离原则,灵活性降低,使用不方便。一般在接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不需要的方法。

1.7 合成复用原则

  在复用时要尽量适合用组合/聚合关系(关联关系),少用继承。

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

  在面向对象设计中可以通过组合/聚合关系或者继承关系来复用已有的设计和实现,但应该首先考虑使用组合/聚合,组合/聚合可以降低类与类之间的耦合度,使系统更加灵活;其次才考虑使用继承,在使用继承时严格遵循里氏替代原则,谨慎使用。
  由于继承复用会将基类的实现细节暴露给子类,破坏了系统的封装性。这种基类某些内部细节对子类对象可见的复用又称为“白箱”复用,如果基类发生改变,子类的实现也不得不跟着改变,从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性,而且被声明为不能继承的类无法使用继承复用。
  组合/聚合关系可以将已有的对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做使成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承而言,其耦合度相对较低,成员对象的变化对新对象影响不大,可以在新对象中根据实际需要有选择的调用成员对象进行操作;合成复用可以在运行时动态进行,新对象可以动态的引用与成员对象类型相同的其他对象。
  一般如果两个类之间的关系是“Has-A”(某个角色具有某一项职责)则应该使用组合/聚合,如果是“Is-A”(一个类是另一个类的一种)则可以使用继承。
  示例:
  某系统中设计的数据库连接方案为,在DBUtil类中封装获取连接方法geConnection(),CustomerDao作为DBUtil类的子类来实现具体连接。方案如下图所示:
在这里插入图片描述
  问题:
  在系统使用过程中需要更换不同的数据库连接,这时候不管是修改基类DBUtil类还是修改子类CustomerDao类都违反了开闭原则。现使用合成复用原则对方案进行重构如下:
在这里插入图片描述
重构后的方案中CustomerDao与DBUtil变为关联关系,采用依赖注入的方式将DBUtil对象注入到CustomerDao中,如果要扩展DBUtil功能,可以通过其子类XXXDBUtil来实现。由于CustomerDao针对DBUtil编程,根据里氏替换原则,DBUtil子类对象可以覆盖DBUtil对象,只需要在CustomerDao中注入子类的对象即可使用子类对象的扩展功能,无需对原有代码进行修改,符合开闭原则。

1.8 迪米特法则

迪米特法则又称为最少知识原则

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

  迪米特法则要求一个软件实体应该尽可能少的与其他实体发生相互作用。应用迪米特法则可以降低系统的耦合度,使类与类之间保持松散的耦合关系。
  迪米特法则中有不要和“陌生人”说话和只与你的直接朋友通信等形式。对于一个对象,迪米特法则中的朋友包括以下几类:
 1. 当前对象本身(this)
 2. 以参数形式传入到当前对象方法中的对象
 3. 当前对象的成员对象
 4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
 5. 当前对象所创建的对象
任何一个对象满足以上条件中的一条即为当前对象的朋友,否则就是陌生人。在应用迪米特法则时,一个对象只与直接朋友发生交互,不与陌生人发生直接交互,这样可以降低系统的耦合度,一个对象的改变不会对其他对象带来更多的影响。
  迪米特法则要求设计系统时应该尽量减少对象间的交互,如果两个对象之间不必彼此通信,那么这两个对象就不应该发生任何的直接交互,如果一个对象需要调用另一个对象的方法,可以通过引入“第三者”来降低现有对象之间的耦合度。
  使用迪米特法则时应该注意:在类的划分上应该尽量创建松散的类;在类的结构设计上应该尽量降低每个类中成员变量和成员函数之间的访问权限;在类的设计上,一个类尽量设计成不变类,对其他类的对象的引用应降到最低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值