写点什么

Java 程序设计(面向对象)- 设计原理,一眼就能看懂的 Java 自学手册

作者:MySQL神话
  • 2021 年 11 月 27 日
  • 本文字数:5051 字

    阅读完需:约 17 分钟

牛呼吸空气


羊呼吸空气


猪呼吸空气


在程序拓展维护的时候发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将 Animal 类细分为陆生动物类 Terrestrial,水生动物 Aquatic,代码如下:


class Terrestrial{


public void breathe(String animal){


System.out.println(animal+"呼吸空气");


}


}


class Aquatic{


public void breathe(String animal){


System.out.println(animal+"呼吸水");


}


}


public class Client{


public static void main(String[] args){


Terrestrial terrestrial = new Terrestrial();


terrestrial.breathe("牛");


terrestrial.breathe("羊");


terrestrial.breathe("猪");


Aquatic aquatic = new Aquatic();


aquatic.breathe("鱼");


}


}


运行结果:


牛呼吸空气


羊呼吸空气


猪呼吸空气


鱼呼吸水


然后发现如果这样修改花销是很大的,不利于后期的拓展和维护,除了将原来的类分解之外,还需要修改客户端。而直接修改类 Animal 来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:


class Animal{


public void breathe(String animal){


if("鱼".equals(animal)){


System.out.println(animal+"呼吸水");


}else{


System.out.println(animal+"呼吸空气");


}


}


}


public class Client{


public static void main(String[] args){


Animal animal = new Animal();


animal.breathe("牛");


animal.breathe("羊");


animal.breathe("猪");


animal.breathe("鱼");


}


}


可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改 Animal 类的 breathe 方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。还有一种修改方式:


class Animal{


public void breathe(String animal){


System.out.println(animal+"呼吸空气");


}


public void breathe2(String animal){


System.out.println(animal+"呼吸水");


}


}


public class Client{


public static void main(String[] args){


Animal animal = new Animal();


animal.breathe("牛");


animal.breathe("羊");


animal.breathe("猪");


animal.breathe2("鱼");


}


}


可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;


比如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。


遵循单一职责原的优点有:


  • 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;

  • 提高类的可读性,提高系统的可维护性;

  • 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。


需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。


单一职责看似简单,实际上在实际运用过程中,会发现真的会出现很多职责扩展的现象,这个时候采用直接违反还会方法上遵循还是完全遵循单一职责原则还是取决于当前业务开发的人员的技能水平和这个需求的时间,如果技能水平不足,肯定会简单的 if else 去解决,不会想什么原则,直接实现功能就好了,这也是为什么在很多小公司会发现代码都是业务堆起来的,当然也有好的小公司代码是写的好的,这个也是不可否认的。不过不管采用什么方式解决,心中至少要知道有几种解决方法。


开闭原则


==========================================================


《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享


========


开闭原则(Open Closed Principle)是 Java 世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。


定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。


含义:一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。


软件实体包括以下几个部分:


  • 项目或软件产品中按照一定的逻辑规则划分的模块

  • 抽象和类

  • 方法


开闭原则是为软件实体的未来事物而制定的对现行开发设计进行约束的一个原则。


注意:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段了。


变化的类型:逻辑变化、子模块变化、可见试图变化。


一个项目的基本路径应该是这样的:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码修改,保持历史代码的纯洁性,提高系统的稳定性。


  • 开闭原则的重要性:

  • 开闭原则对测试的影响


开闭原则可是保持原有的测试代码仍然能够正常运行,我们只需要对扩展的代码进行测试就可以了。


  • 开闭原则可以提高复用性


在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。


  • 开闭原则可以提高可维护性

  • 面向对象开发的要求


如何使用开闭原则:


  • 抽象约束


第一,通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的 public 方法;


第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;


第三,抽象层尽量保持稳定,一旦确定即不允许修改。


  • 元数据(metadata)控制模块行为


元数据就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。


Spring 容器就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control)


  • 制定项目章程


在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。


  • 封装变化


对变化的封装包含两层含义:


第一,将相同的变化封装到一个接口或者抽象类中;


第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。


依赖倒置原则 (Dependence Inversion Principle)


=====================================================================================================


所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体。实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。


定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。


通俗点说:要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。


问题由来:类 A 直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类 A 的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻辑;类 B 和类 C 是低层模块,负责基本的原子操作;假如修改类 A,会给程序带来不必要的风险。


解决方案:将类 A 修改为依赖接口 I,类 B 和类 C 各自实现接口 I,类 A 通过接口 I 间接与类 B 或者类 C 发生联系,则会大大降低修改类 A 的几率。


依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在 java 中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。


依赖倒置原则的核心思想是面向接口编程,我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。


传递依赖关系有三种方式:


  • 使用的方法是接口传递

  • 构造方法传递

  • setter 方法传递


在实际编程中,我们一般需要做到如下三点:


  • 低层模块尽量都要有抽象类或接口,或者两者都有。

  • 变量的声明类型尽量是抽象类或接口。

  • 使用继承时遵循里氏替换原则。


依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。


接口隔离原则 (Interface Segregation Principle)


======================================================================================================


其原则字面的意思是:使用多个隔离的接口,比使用单个接口要好。本意降低类之间的耦合度,而设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。


原定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。


问题由来:类 A 通过接口 I 依赖类 B,类 C 通过接口 I 依赖类 D,如果接口 I 对于类 A 和类 B 来说不是最小接口,则类 B 和类 D 必须去实现他们不需要的方法。


解决方案:将臃肿的接口 I 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。


如图所示:



上图就没有实现接口隔离,类 B 和 类 D 中都会实现不是自己的方法。


如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口 I 进行拆分。在这里我们将原有的接口 I 拆分为三个接口,拆分后的设计如下图所示:



接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为 3 个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。


说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。


其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。


其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。


采用接口隔离原则对接口进行约束时,要注意以下几点:


  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。

  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。


运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。


合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)


=================================================================================================================


定义:在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用这些对象的目的。应首先使用合成/聚合,合成/聚合则使系统灵活,其次才考虑继承,达到复用的目的。而使用继承时,要严格遵循里氏代换原则。有效地使用继承会有助于对问题的理解,降低复杂度,而滥用继承会增加系统构建、维护时的难度及系统的复杂度。


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

最后

手绘了下图所示的 kafka 知识大纲流程图(xmind 文件不能上传,导出图片展现),但都可提供源文件给每位爱学习的朋友



本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

用户头像

MySQL神话

关注

还未添加个人签名 2021.11.12 加入

还未添加个人简介

评论

发布
暂无评论
Java程序设计(面向对象)- 设计原理,一眼就能看懂的Java自学手册