Java是一门面向对象的编程语言,为了充分发挥面向对象编程的好处,就要遵循其相关设计原则,以求高效快速的编写高性能的Java代码。本文主要学习面向对象的六大设计原则。
六大设计原则
六大设计原则的单词首字母可组成单词SOLID(可靠的),又称SOLID原则。
- 单一职责原则(SRP, Single Responsibility Principle)
- 开放封闭原则(OCP, Open-Close Principle)
- 里氏替换原则(LSP, Liskov Substitution Principle)
- 迪米特法则(LOD, Law of Demeter),又称最少知识原则(LKP, Least Knowledge Principle)
- 接口隔离原则(ISP, Interface Segregation Principle)
- 依赖倒置原则(DIP, Dependence Inversion Principle)
1 单一职责原则(SRP)
1.1 定义:
There should never be more than one reason for a class to change.(一个类,只应有一个引起它变化的原因。)
1.2 理解:
一个类只负责一个功能的相应职责。或者说,在类、接口、方法层面的职能都应是单一的。
1.3 作用:
- 降低类的复杂度:类的职责单一,逻辑相对简单很多;
- 提高类的可读性:复杂度降低,利于可读性;
- 提高类的可维护性:可读性提高,更容易维护;
- 降低类的变更风险:修改一个接口只对相应的实现类有影响,对其他的接口无影响。
注意:过于追求职责单一,可能会造成类的大爆炸。
1.4 其他:
实际项目开发中,场景多样很难做到完全的单一职责原则。因此,要尽量做到接口和方法的单一职责,类尽量简短。
2 开放封闭原则(OCP)
2.1 定义:
Software entities like classes, modules and functions should be open for extension but closed for modifications.(一个软件实体的类、模块、函数,对扩展开放,对修改关闭。)
2.2 理解:
当需求改变时,我们应扩展原有代码,而不是修改原有代码。因为修改可能会引发更多问题。一个较好的方式是:用抽象构建框架,用实现扩展细节。当需要修改时,直接通过抽象派生一个具体类去实现修改。
2.3 作用:
- 提高稳定性:不修改原有代码;
- 提高灵活性;
- 提高可扩展性。
开闭原则可以指导我们建立一个稳定、灵活、可扩展的系统。
2.4 其他:
2.4.1 如何使用开闭原则?
- 抽象约束:通过接口或抽象类描述一组可能变化的行为,并通过不同的实现进行扩展。
- a. 约束扩散:对扩展进行边界限定,不允许出现接口或抽象类中不存在的public方法;
- b. 参数抽象化:引用对象尽量使用接口或抽象类,这是实现里氏替换原则的基础;
- c. 抽象层保持稳定:接口或抽象类一旦确定就尽量不要修改。
- 元数据控制模块行为:尽量使用元数据来控制程序的行为,减少重复开发。
- 元数据:用来描述环境和数据的数据,即配置参数;参数可以从文件、数据库中获得。
- 制定项目规范:对项目来说,约定优于配置,项目规范是所有成员都必须遵守的约定,这比通过接口或抽象类进行约束效率更高,也不影响扩展性。
- 封装变化:找出可能有变化或不稳定的点,为这些变化点创建稳定的接口,即这些变化点是受保护的。
- 封装变化包含两层含义:(1)将相同的变化封装到一个接口或抽象类中;(2)将不同的变化封装到不同的接口或抽象类中。
3 里氏替换原则(LSP)
3.1 定义:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)
3.2 理解:
代码中,所有父类出现的地方,都能用其子类替换,且不会产生任何错误和异常,但子类出现的地方不一定能用父类替换。子类可以扩展父类的功能,但不能改变父类原有的功能。
继承是侵入性的,父类对于子类是透明的,这在一定程度上违反了封装。里氏替换原则为良好的继承定义了一个规范,包含4层含义:
- 子类必须实现父类的抽象方法,但不能覆盖父类的非抽象方法;
- 子类可以增加自己特有的方法;
- 子类重载父类的方法时候,方法的形参要比父类更宽松;
- 子类实现父类的抽象方法时,方法的返回值要比父类更严格。
3.3 作用:
- 是实现开闭原则的重要方式之一;
- 克服继承中重写造成的可复用性变差的缺点;
- 提高稳定性:即类的扩展不会影响已有的功能。
里氏替换原则反映了基类与子类间的关系,是继承复用的基础,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。
3.4 其他:
- 里氏替换原则依赖于面向对象的继承、多态两大特性。
- 在项目中,采用里氏替换原则时,尽量避免子类的“个性”,目的是为增强代码的健壮性。
4 迪米特法则(LOD)
又称最少知识原则(LKP, Least Knowledge Principle)
4.1 定义:
Only talk to you immediate friends.(只与直接的朋友通信。)
4.2 理解:
一个对象应该对其他对象保持最少的了解,即尽量少地与其他对象通信,只与必需的有直接关系的对象通信。
在迪米特法则中,一个对象的直接朋友包括以下几类:
- 当前对象本身(this);
- 当前对象方法中的参数对象;
- 当前对象的成员对象;
- 当前对象的集合成员中的元素对象;
- 当前对象所创建的对象。
迪米特法则可以限制对象间通信的广度和深度。如果一个系统应用了迪米特法则,则一个对象的改变不会给太多其他对象造成影响。
4.3 作用:
- 降低耦合度,减少对象间的相互依赖,使对象间保持松散的耦合关系(弱耦合)。
4.4 其他:
4.4.1 什么是耦合?
耦合指对象之间的联系。根据联系的紧密程度可分为强耦合和弱耦合。强耦合:对象间存在直接关系;弱耦合:对象间只存在间接关系。
4.4.2 什么是解耦?
解耦指降低对象间的耦合度,即将强耦合变为弱耦合的过程。
4.4.3 为什么要解耦?
对象间的耦合越高,维护成本越高。
4.4.4 如何解耦?
解耦的核心是封装和多态,包含但不限于以下几点:
- 利用多态:如使用接口、类的向上转型等;
- 利用封装:引入框架,如Spring的IOC(控制反转)等;
- 利用设计模式:适配器模式、观察者模式等;
- 避免使用全局变量;
- 避免相似函数。
5 接口隔离原则(ISP)
5.1 定义:
The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)
5.2 理解:
在程序中,针对不同的功能,建立多个职能专一的接口。这些接口使用起来更加灵活,实用性更高。接口隔离原则主要用来约束接口,即将接口根据功能进行分类,使不同功能的接口函数在不同的接口中。从某种程度来讲,接口隔离原则可以看做是接口层的单一职责原则。
单一职责原则与接口隔离原则的区别:
- 单一职责原则强调职责,主要是约束类,其次才是接口和方法,是针对实现和细节;
- 接口隔离原则强调接口,主要是约束接口,是针对抽象,针对程序的整体架构。
5.3 作用:
- 降低耦合度;
- 提供稳定性;
- 提高灵活性;
- 提高可扩展性。
5.4 其他:
5.4.1 接口隔离原则注意事项:
- 接口粒度要小:接口小,但要有限度,避免接口过多而使设计复杂化。如一个接口只服务于一个子模块或业务功能。
- 接口要高内聚:即接口中尽量少提供public方法,减少对外交互,这样风险就少,利于降低维护成本。
- 定制服务:系统模块之间必然会有耦合,有相互访问的接口。因此,在设计接口时,只提供访问者需要的方法,即定制服务,按需拆分接口。
6 依赖倒置原则(DIP)
6.1 定义:
High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.(高层模块不应依赖于底层模块,二者都应依赖于抽象,抽象不应依赖于细节, 细节应依赖于抽象。)
6.2 理解:
在java中,依赖倒置原则中的抽象指接口或抽象类,细节指实现类,表现为:
- 模块间的依赖关系通过抽象产生,实现类间的依赖关系也通过抽象产生;
- 接口或抽象类不依赖于实现类;
- 实现类依赖于接口或抽象类。
依赖倒置原则的核心是面向接口编程,即通过抽象使各个模块实现独立,互不影响,实现模块间的松耦合。具体的说,就是通过接口或抽象类进行变量、参数、方法等的声明,而实现类负责实现。
大多数情况下,开闭原则、里氏替换原则和依赖倒置原则,三者会同时出现。开闭原则是目标,里氏替换原则是基础,依赖倒置原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题的角度不同而已。
6.3 作用:
- 提高可扩展性;
- 提高可维护性;
- 提高灵活性。
6.4 其他:
6.4.1 依赖注入(DI, Dependency Injection)
依赖注入是指当一个对象要与其他对象发生依赖关系时,通过注入的方式获取所依赖的对象。常用的注入方式有三种:
- 构造注入:指通过构造函数来传入具体类的对象;
- 设值注入:指通过Setter方法来传入具体类的对象;
- 接口注入:指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用抽象类型,运行时传入具体类型对象。
在使用依赖倒置原则时,我们需要针对抽象编程,通过依赖注入的方式将实现类的对象注入到其他对象中。
6.4.2 如何使用依赖倒置原则?
要注意以下几点:
- 每个具体类都尽量对应一个接口或抽象类,或两者兼备。因为抽象是依赖倒置的基本要求。
- 变量的声明类型尽量用接口或抽象类。有特殊情况,如:工具类,一般不需要接口或抽象类;使用类的clone方法,就必须用实现类,这是JDK的规范。
- 任何类都不应从具体类派生。
- 尽量不要重写基类的方法。
- 结合里氏替换原则使用。
另外:
- 过于坚持DIP原则会生产出大量的类(抽象类和接口)。
- 依赖倒换原则假定所有具体类都是可变化的,但是实际并不总是这样。
#网易云课堂 #微专业 #Java