写了这么久代码你了解Java面向对象的设计原则吗?(三)

简介: 写了这么久代码你了解Java面向对象的设计原则吗?

接口隔离原则

  • 接口隔离原则要求我们将一些较大的接口进行细化,使用多个专门的接口来替换单一的总接口。

接口隔离原则的定义

  • 客户端不应该依赖那些它不需要的接口。
  • 注意,在该定义中的接口指的是所定义的方法。
  • 另一种定义,一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。

接口隔离原则分析

实质上,接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象﹔另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,如Java语言里面的interface。对于这两种不同的含﹐ISP的表达方式以及含义都有所不同。


当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。此时,可以把接口理解成角色,一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。


如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。在面向对象编程语言中,如果需要实现一个接口,就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便。为了使接口的职责单一,需要将大接口中的方法根据其职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可。


使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下﹐接口中的方法越少越好。可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。


接口隔离原则实例

  • 下面通过一个简单的实例来加深对接口隔离原则的理解。

实例说明

  • 下图展示了一个拥有多个客户类的系统,在系统中定义了一个巨大的接口AbstractService来服务所有的客户类。

750b14a17b1f4235ab8ee0b05b1f3a7f.png

如果客户类ClientA只须针对方法 operatorA()进行编程,但由于提供的是一个胖接口,AbstractService的实现类ConcreteService必须实现在AbstractService中声明的所有三个方法,而且在ClientA中除了能够看到方法 operatorA() ,还能够看到与之不相关的方法operatorB()和 operatorC() ,在一定程度上影响系统的封装性。因此,可以使用接口隔离原则对其进行重构。


实例解析


由于在接口AbstractService中三个不同的方法分别对应三类不同的客户端,因此需要将该接口进行细化,以确保每一类用户都具有与之对应的专门的接口,可以将该接口分割成三个小接口,如图下图所示。


55483dd0e86a41b89ea2f6c16770b4cf.png


通过对AbstractService接口的细化,我们可以将其分割为三个专门的接口;AbstractServiceA ,A bstractServiceB和AbstractServiceC,在每个接口中只包含一个方法,用于对应一个客户端。在实际使用过程中,如果一个客户端对应多个方法,可以将这几个方法封装在同一个小接口中。接口实现类ConcreteService可以一次性实现这三个接口,也可以提供三个接口实现类分别实现这三个接口。无论是使用一个实现类还是使用三个实现类,对于ClientA等客户端类而言没有任何区别,因为它们是针对抽象的接口编程,只能看到与自己相关的业务方法,不能访问其他方法,因此保证系统具有良好的封装性。同时,无须关心一个业务方法的改变会给一些不相关的类造成影响,因为这些类根本无法访问该方法。

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


合成复用原则

  • 合成复用原则是面向对象设计中非常重要的一条原则。为了降低系统中类之间的耦合度,该原则倡导在复用功能时多用关联关系,少用继承关系。

合成复用原则定义

  • 合成复用原则(Composite Reuse Principle,CRP)又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP),其定义为:尽量使用对象组合,而不是继承来达到复用的目的。

合成复用原则分析


GoF提倡在实现复用时更多考虑用对象组合机制,而不是用类继承机制。通俗地说,合成复用原则就是指在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分﹔新对象通过委派调用已有对象的方法达到复用其已有功能的目的。简言之,要尽量使用组合/聚合关系,少用继承。

在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,这两种复用机制的特点如下:


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


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


组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用,其次才考虑继承。在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。


合成复用原则实例

  • 下面通过一个简单实例来加深对合成复用原则的理解。

实例说明

  • 某教学管理系统的部分数据库访问类设计如下图所示。


f2bccbe546ef4bf7b518fb3308bf5089.png

在该类图中, DBUtil类用于连接数据库,它提供了一个 getConnection()方法,用于返回一个Connection类型的数据库连接对象。由于在StudentDAO、TeacherDAO等类中都需要连接数据库,因此需要复用getConnection()方法,在本设计方案中, StudentDAO、TeacherDAO等数据访问类直接继承 DBUtil 类,复用其中定义的方法。

如果需要更换数据库连接方式,如原来采用JDBC连接数据库,现在采用数据库连接池连接,则需要修改 DBUtil类源代码。如果StudentDAO采用JDEC连接,但是TeacherDAO采用连接池连接,则需要增加一个新的 DBUtil类,并修改StudentDAO或TeacherDAO的源代码,使之继承新的数据库连接类,这将违背开闭原则,系统扩展性较差。

下面使用合成复用原则对其进行重构


实例解析


根据合成复用原则,我们可以使用组合/聚合复用来取代继承复用,如下图所示。


60133dfe983a4c38be8d117400643e72.png

StudentDAO和TeacherDAO类与DBUril 类不再是继承关系,而改为聚合关联关系,并增加一个setDBOperator()方法来给DBUtil类型的成员变量dBOperator赋值。如果需要改为另一种数据库连接方式,只需要给DBUtil 增加一个子类,如NewDBUtil,在该子类中覆盖getConnection()方法,再在客户类中调用setDBOperator()方法时注入子类对象即可。如果希望系统更加灵活一点,可以在客户类中针对 DBUtil编程,而将具体类类名存储在配置文件中,DBUtil类及其子类都可以直接应用于该系统。用户无须修改任何源代码,只需修改配置文件即可完成新的数据库连接方式的使用,完全符合开闭原则。


迪米特法则

  • 迪米特法则用于降低系统的耦合度,使类与类之间保持松散的耦合关系。

迪米特法则定义

迪米特法则(Law of Demeter,LoD)又称为最少知识原则(Least Knowledge Principle,LKP),它有多种定义方法,其中几种典型定义如下:

  1. 不要和 “陌生人” 说话。
  2. 只与你的直接朋友通信。
  3. 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

迪米特法则分析

简单地说,迪米特法则就是指一个软件实体应当尽可能少地与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少地影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。

在迪米特法则中,对于一个对象,其朋友包括以下几类:


  1. 当前对象本身 this ;
  2. 以参数形式传入到当前对象方法中的对象;
  3. 当前对象的成员对象;
  4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
  5. 当前对象所创建的对象。

任何一个对象如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。迪米特法则可分为狭义法则和广义法则。在狭义的迪米特法则中,如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用,如图下图所示。


90cad68bc9e04fed92df01f3c553461c.png

在上图中,Object A 与Object B存在依赖关系,ObjectC是Object B的成员对象,根据迪米特法则,Object A只能调用ObjectB中的方法,而不允许调用Object C中的方法,因为它们之间不存在直接引用关系。根据迪米特法则,不允许出现a.method1(). method2()或者a. b. method()这样的调用方式,只允许出现 a. method(),也就是在方法调用时只能够出现一个“.”(点号)。


狭义的迪米特法则可以降低类之间的耦合,但是会在系统中增加大量的小方法并散落在系统的各个角落,它可以使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联,但是也会造成系统的不同模块之间的通信效率降低,使得系统的不同模块之间不容易协调。


广义的迪米特法则就是指对对象之间的信息流量﹑流向以及信息的影响的控制,主要是对信息隐藏的控制。信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化,使用和修改,同时可以促进软件的复用,由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显。


迪米特法则的主要用途在于控制信息的过载。在将迪米特法则运用到系统设计中时,要注意下面的几点:


1.在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低﹐就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及。

2.在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限。

3.在类的设计上,只要有可能,一个类型应当设计成不变类。

4.在对其他类的引用上,一个对象对其他对象的引用应当降到最低。


迪米特法则实例

  • 下面通过一个简单实例来加深对迪米特法则的理解。

实例说明

  • 某系统界面类(如Form1 ,Form2等类)与数据访问类(如DAO1,DAO2等类)之间的调用关系较为复杂,如下图所示。

1ae3bda290ec411ea6e80902fceab115.png

由于存在复杂的调用关系,将导致系统的耦合度非常大,重用现有类比较困难,增加新的界面类或数据访问类也比较麻烦。现需要降低界面类和业务逻辑类之间的耦合度,可使用迪米特法则对系统进行重构。

实例解析


为了降低界面类与数据访问类之间的耦合度,可以在它们之间引人一系列控制类(如Controller1,Controller2等类),由控制类来负责控制界面类对业务逻辑类的访问,重构之后的类图如下图所示。


95b9f242882447478940ea8c37f0d8dc.png

在重构过的上图中,由于控制类的引入,界面类与数据访问类之间不存在直接引用关系。如果增加一个新的界面类如Form6,需要引用DAO2,DAO3和DAO4,原来需要建立三个引用关系,而有了控制类后,只需要直接引用控制类Controller2即可。如果需要增加新的数据访问类,可以对应增加新的控制类或者修改现有控制类,无须修改原有界面类。系统具有较好的灵活性,且可以很方便地重用现有的界面类和数据访问类。


总结


  1. 对于面向对象的软件系统设计来说,在支持可维护性的同时,需要提高系统的可复用性。
  2. 软件的复用可以提高软件的开发效率,提高软件质量,节约开发成本,恰当的复用还可以改善系统的可维护性。
  3. 单一职责原则要求在软件系统中,一个类只负责一个功能领域中的相应职责。
  4. 开闭原则要求一个软件实体应当对扩展开放,对修改关闭,即在不修改源代码的基础上扩展一个系统的行为。
  5. 里氏替换原则可以通俗表述为在软件中如果能够使用基类对象,那么一定能够使用其子类对象。
  6. 依赖倒转原则要求抽象不应该依赖于细节,细节应该依赖于抽象,要针对接口编程,不要针对实现编程。
  7. 接口隔离原则要求客户端不应该依赖那些它不需要的接口,即将一些大的接口细化成一些小的接口供客户端使用。
  8. 合成复用原则要求复用时尽量使用对象组合,而不使用继承。
  9. 迪米特法则要求一个软件实体应当尽可能少地与其他实体发生相互作用。


注意:本文画图工具采用 ProcessOn 链接:https://www.processon.com/diagrams


1c7e34d230e94688a95bf380fb49993b.png

这也不是广告,给大家推荐的工具罢了,本文中的图片纯手绘如有错别字请见谅,评论区指出我会及时更改的。

40347cb901d7462d8dad0222fe8d7592.gif


好了,到此Java面向对象设计原则就总结完毕了,大家看完之后别忘了一键三连关注下方公众号哦 ❤



相关文章
|
8天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
22天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
35 5
Java反射机制:解锁代码的无限可能
|
19天前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
52 3
|
24天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
68 10
|
20天前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
18天前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别
|
21天前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
14 2
|
26天前
|
搜索推荐 Java 数据库连接
Java|在 IDEA 里自动生成 MyBatis 模板代码
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
30 6
|
12天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
9天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
31 9
下一篇
无影云桌面