学习一些经典的设计原则,再去学习设计模式,会事半功倍,设计模式是设计原则的实践总结提炼。
设计原则看似简单,但是在实际应用中却不是很好实践。需要理解透彻。
学习时需要取思考和掌握设计原则的设计初衷,能解决哪些问题,有哪些应用场景。
SOLID设计原则
S:单一职责原则(Single Responsibility Principle)
A class should have one and only one reason to change, meaning that a class should have only one job.
单一职责,设计原则中最重要最基础的原则之一
如何理解单一职责原则
一个类只负责完成一个职责或功能,不要设计大而全的类,要设计粒度小,功能单一的类。主要目的是实现代码高内聚,低耦合,提高代码的可读性,可维护性,复用性
如何判断类的职责是否足够单一
不同阶段,不同需求,不同场景,对于一个类的职责是否单一,可能会有不同的判定结果。
比如下面几种就有可能说明类的设计不够单一
- 类中的代码行数,属性,函数过多
- 类依赖其他类过多,或者依赖类的其他类过多
- 私有方法过多
- 比较难给类起一个合适的名字
- 类中大量的方法都是集中在类中的某几个属性
是否越单一越好
不是,还要考虑类的内聚性,有时为了单纯追求职责单一,强行将类进行拆分,导致内聚性不足,可读性变差,反倒得不偿失。如果真要隔离,可以考虑实现接口的方式,使用接口隔离原则来进行隔离
O:开闭原则(Open Closed Principle)
Objects or entities should be open for extension but closed for modification.
对拓展开放,对修改关闭
开闭原则也是设计原则最基础的原则之一,大多数为了解决代码拓展性问题而总结出来的设计模式,都是以开闭原则为指导原则。
理解开闭原则
简单理解一下,当添加一个新功能时,最好是在已有代码基础上拓展代码(新增模块,类,方法等),而非修改已有代码(修改模块,类,方法等)。
但是需要认识到的是,当我们新增功能的时候,不可能任何模块,类,方法的代码都不修改,我们要做的就是让修改操作更集中,更上层。以最小的修改代价完成新功能的开发。
同样的代码改动,在粗粒度下,可能被认为是修改,在细粒度下,可能被认为是拓展,需要根据实际业务进行辨别。
如何做到“对拓展开放,对修改关闭”
- 要时刻具备拓展意识,抽象意识,封装意识。
- 在设计代码时,事先根据业务需求留好拓展点,以便在未来需求变更时,在不改变代码整体结构,做到最小改动的情况下,将新功能灵活的插入到拓展点上。
如何提高拓展性
- 使用多态,依赖注入,基于接口而非实现编程。
- 使用设计模式:装饰,策略,模板,责任链,状态。
优点
- 减少了因新增功能而导致的对已有功能的影响,
- 集中修改,方便理解
L:里氏替换原则(Liskov Substitution Principle)
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
子类可以替换程序中父类出现的任何地方,且保证原来程序的逻辑行为不变及正确性不被破坏
和多态的区别
- 多态是面向对象编程的一大特性,是一种代码实现的思路
- 里氏替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证再替换父类的时候,不改变原有程序的逻辑和不破坏原有程序的正确性
- 简单理解就是,不要对父类已实现方法进行重写,子类只添加拓展方法。
违反里氏替换原则
- 子类违背父类声明要实现的功能。
- 子类违背父类对输入,输出,异常的预定
- 子类违背父类注释中所罗列的任何特殊说明
- 最简单的就是不要取重写父类方法
I:接口隔离原则(Interface Segregation Principle)
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
偏向接口层面的职责单一,交互时不暴露无用的方法接口
理解接口隔离的“接口”
这里的接口有三种不同的理解
- 理解为一组接口的集合:如果部分接口只被部分调用者使用,我们就要把这部分接口隔离出来,单独给这部分调用者使用,而不是让调用者也能看到不会被用到的接口。
// 如果只想给外部系统暴露查询操作,则转化成UserService即可做到接口隔离
public UserServiceImpl implements UserService,RestrictedUserService{
}
// 这里UserService接口中定义常用查询方法
public interface UserService{}
// 这里受限用户几口定义一些删除,修改方法
public interface RestrictedUserService{}
- 理解位单个API接口或函数:部分调用者只需要函数中的部分功能,那我们就把函数拆成更细粒度的多个函数。
- 理解为OOP中的接口,接口的设计要尽量单一,不要让接口的实现者和调用者,依赖不需要的接口函数。
- 要利用好接口可以多实现的特性
接口隔离与单一职责的区别
- 单一职责针对的是模块,类,接口的设计,接口隔离更侧重于接口的设计。
- 接口隔离提供了一种判断接口职责是否单一的标准,如果调用者只是用部分接口或接口的部分功能,那接口的设计就不够指责单一
D:依赖反转原则(Dependency Inversion Principle)
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
也叫依赖倒置,高层模块不依赖低层模块,应该使用抽象关联,抽象不依赖具体实现,具体实现依赖抽象
简单来说,在调用链上,调用者属于高层,被调用者数与低层。在平时的业务代码中高层模块依赖低层模块没有任何问题。实际上,这条原则主要还是指导框架层面的设计。
比如tomcat容器和web应用之间没有直接的依赖关系,两者都依赖同一个抽象,也就是servlet规范。Servlet规范不依赖tomcat容器和应用程序的实现细节,反而是容器和应用程序依赖Servlet规范。
控制反转
控制值得是对程序程序执行流程的控制,而反转指的是没有使用框架之前,程序员自己控制整个程序的执行,使用框架后,整个程序的控制流程可以通过框架来控制。流程的控制权从程序员反转到了框架上。
模板方法模式就是个很好的例子,上层框架来出来执行流程,而自己只需要实现流程过程中的拓展点即可。
依赖注入
指不通过new的方式在类内部创建依赖类的对象,而是将对象在外部创建好后,通过构造函数,函数参数等方式传递(注入)给类来使用。
依赖注入框架
通过简单配置所有需要的类及其类与类之间的依赖关系,就可以实现由框架来自动创建对象,管理对象生命周期,依赖注入等原本需要程序员来做的事情。
其他常见设计原则
KISS原则(Keep It Simple, Stupid)
- KISS原则是英语Keep It Simple, Stupid 的首字母缩略字,是一种归纳过的经验原则。KISS 原则是指在设计当中应当注重简约的原则。总结工程专业人员在设计过程中的经验,大多数系统的设计应保持简洁和单纯,而不掺入非必要的复杂性,这样的系统运作成效会取得最优;因此简单性应该是设计中的关键目标,尽量回避免不必要的复杂性。同时这原则亦有应用在商业书信、设计电脑软件、动画、工程上
- KISS原则是保持代码可读和可维护的重要手段
- 不要使用同事可能不懂的技术来实现代码;
- 不要重复造轮子,要善于使用已经有的工具类库;
- 不要过度优化。
- KISS原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug比较难隐藏。即便出现bug,修复起来也比较简单。
YAGNI原则(You aren't gonna need it)
不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。
当前不需要就不要做
DRY原则(Don’t Repeat Yourself)
不要写重复的代码。
常见的有:实现逻辑重复、功能语义重复和代码执行重复
提高代码复用性
- 减少代码耦合
- 满足单一职责原则
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板等设计模式
迪米特法则(Law of Demeter)
最小知识原则
高内聚,低耦合
基于最小接口而非最大实现编程
每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
不该有直接依赖关系的类之间,不要有依赖;
有依赖关系的类之间,尽量只依赖必要的接口
这里和接口隔离原则类似