前言
谈到设计模式,大家应该都不陌生了,可能有的同学23种设计模式张口就来,亦或者有的同学只了解了几种常用的设计模式,难道就能得出谁更优秀了吗?显然这些不能作为谁比较优秀的衡量标准,换句话说搞清楚为什么有这么多设计模式?这些设计模式为了解决什么问题才是应该考虑的正事。
在日常开发中,有时候为了解决某些问题,部分同学可能已经在不知不觉中使用了对应的设计模式的思想(可能说不出具体用了什么设计模式)。设计模式在不同场景下的掌握要求不同,比如说在面试中我们可能需要就设计模式能说会道一番,在项目实战中我们需要关注如何用好设计模式,核心还是设计模式所涉及到的思想。
什么场景下用设计模式?
很多同学在听到代码重构、架构设计等可能会想到设计模式,认为多用设计模式就是好的重构手法、就是优秀的架构设计,其实非也,没有底层思想的沉淀,把握不好应用场景,使用设计模式生搬硬套地作出多余的设计,不但解决不了问题反而降低代码的可读性。
那么底层思想是什么?什么场景下使用设计模式?
- 底层思想其实就是
设计原则
,而设计原则
则是面向对象编程
基于现实背景衍生出来的一套规则,用来解决实际开发中的痛点,是所有设计模式的底层思想。 - 至于什么场景下用设计模式,只能说“合适的场景”,根本没有硬性标准。需要在实践中不断思考,因为设计模式就是为了解决实际开发中的痛点,开发中痛不痛自行体会。
有哪些设计原则?
六大设计原则
- 单一职责原则(Single Responsibility Principle);
- 开闭原则(Open Closed Principle);
- 里氏替换原则(Liskov Substitution Principle);
- 迪米特法则(Law of Demeter),又叫“最少知道法则”;
- 接口隔离原则(Interface Segregation Principle);
- 依赖倒置原则(Dependence Inversion Principle)。
单一职责原则
单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(不仅适用于接口和类,也适用于方法);如果不进行拆分,一个对象承担了太多的职责,至少存在以下两个缺点:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成代码冗余。
单一职责的好处:
- 降低类的复杂性,职责单一清晰明确;
- 提高代码可读性、可维护性;
- 降低因程序变更引起的风险,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
单一职责原则既是最简单也是最难运用的原则,需要设计人员根据类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
开闭原则
开闭原则是指一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。也就是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。用抽象构建框架,用实现扩展细节。
开闭原则的使用建议:
-
在思想上要具备扩展意识、抽象意识、封装意识。
这些意识的培养依赖我们对面向对象的理解、对业务的掌握度,以及长期的经验积累... 这要求我们在写代码的时候后,要多花点时间往前多思考一下,未来可能有哪些需求变更,识别出代码的易变部分与不易变部分,合理设计代码结构,事先留好扩展点,以便在未来不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上。 -
在方法上可以通过多态、依赖注入、面向接口编程等方式来实现代码的可扩展性。
做到“对扩展开放、对修改关闭”。我们要将可变部分抽象出来以隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。
修改代码一定违反开闭原则吗?
开闭原则中对于修改是封闭的
并非是一个绝对的概念。
-
修复缺陷所做的改动
缺陷在软件中很常见,是不可能完全消除的。当缺陷出现时,就需要我们修复现有的代码。软件修复明显倾向于实用主义而不是坚持开闭原则。 -
客户端无法感知到的改动
如果一个类的改动会引起另一个类的改动,那么这两个类就是紧密耦合的。相反,如果一个类的修改总是独立的,并不会引起其他类的改动,那么这些类就是松散耦合的。我们要记住,任何情况下,松散耦合都比紧密耦合要好。如果我们对现有代码的修改不会影响客户端代码,那么也就谈不上违背开闭原则。 -
修改还是扩展
从开闭原则定义中,我们可以看出,开闭原则可以应用在不同粒度的代码中,可以是模块、类或者方法(及其属性)。同样一个代码改动,在粗代码粒度下,可以被认定为“修改”,但在细代码粒度下,又可以被认定为“扩展”。
实际上,当纠结于某个代码改动是“修改”还是“扩展”的时候,就已经背离了设计原则的初衷,开闭原则的本质目的就是为了让我们的代码更具有扩展性,更容易维护,如果我们可以很容易的完成修改,又不会影响到既有的代码,就可以认为这是一个合理的改动。
里氏替换原则
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但尽量不改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
里氏替换原则的使用建议:
- 子类可以实现父类的抽象方法,但尽量不重写父类的非抽象方法;
- 子类中可以增加自己特有的方法与属性;
- 当子类需要重载父类中的方法时,子类方法的形参(入参)要比父类方法输入的参数更宽松(范围更广);
- 当子类的方法重写或实现父类方法时,方法的的输出/返回值可以被缩小,但是不能放大。
在面向对象的语言中,继承是实现代码重用的重要手段,是面向对象的三大特征之一,它有如下优点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的属性和方法;
- 提高代码的复用性;
- 子类可以形似父类,但又异于父类;
- 提高代码的可扩展性。
凡事皆有利弊,“继承”也不例外,有以下弊端:
- 继承是侵入性的。只要继承,就必须拥有父类的属性和方法。
- 降低代码的灵活性。子类会多一些父类的约束。
- 增强了耦合性。当父类的常量、变量、方法被修改时,需要考虑子类的修改。
为了让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦,引入了里氏替换原则(LSP)。(此处是结合设计模式中里氏替换原则提到了继承,关于慎用继承,使用组合和委托等方式此处不做讲述,后期专门讲解)
依赖倒置原则
依赖倒置原则在Java语言中的表现是:
- 模块间的依赖通过抽象发生,实现类之间不直接发生依赖关系,其依赖关系是通过接口或抽象类产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类。 换句话说就是“面向接口编程”。
依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
依赖倒置原则的使用建议:
- 每个类尽量都有接口或抽象类,或者接口和抽象类两者都具备。
- 变量的表面类型尽量是接口或抽象类。
- 任何类都不应该从具体类派生。
- 尽量不要重写基类的方法。如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要重写。
- 结合里氏替换原则使用。
接口隔离原则
接口隔离原则就是客户端不应该依赖它不需要的接口,或者说类间的依赖关系应该建立在最小的接口上,不要建立臃肿庞大的接口通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。(接口尽量细化,同时接口中的方法尽量少。)
接口隔离原则的使用建议:
- 接口尽量小,但是要有限度。
一个接口只服务于一个子模块或业务逻辑。 - 为依赖接口的类定制服务。
只提供调用者需要的方法,屏蔽不需要的方法。 - 了解环境,拒绝盲从。
每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同,深入了解业务逻辑。 - 提高内聚,减少对外交互。
高内聚就是要提高接口、类、模块的处理能力,减少对外的交互。要求在接口中尽量少公布public方法,接口是对外的承诺,承诺地越少对系统开发越有利,变更的风险也就越少,同时也有利于降低成本。
迪米特法则
迪米特法则也叫最少知道法则:一个对象应该对其他对象有最少的了解。换句话说如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则的优点:
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 提高了类的可复用率和系统的扩展性。
迪米特法则的表现:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
迪米特法则的使用建议:
- 在类的划分上,应该创建弱耦合的类。
类与类之间的耦合越弱,就越有利于实现可复用的目标。 - 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
总结六大设计原则
-
单一职责原则:一个类或接口只承担一个职责。
-
开闭原则:对软件实体的改动,最好用扩展而非修改的方式。
-
里氏替换原则:在继承类时,子类可以实现父类的抽象方法,但尽量不重写父类的非抽象方法,同时尽量不要暴露自己的public方法供外界调用。
-
依赖倒置原则:高层模块不应该依赖于低层模块,而应该依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。
-
接口隔离原则:不要对外暴露没有实际意义的接口。
-
迪米特法则:尽量减少对象之间的交互,从而减小类之间的耦合。
结论:
设计原则是基于面向对象扩展出来的一套思想,用来解决开发中痛点,好的架构设计与重构手法其实就是基于设计原则反复进行思考以及设计的产物,只有掌握好设计原则、设计模式才能做出优秀的架构。
那如何应用设计模式呢?
- 业务决定架构
- 不要过度设计
- 面向接口编程
任何架构模式都没有最好、最优之说,只有最适合当前业务的才是好架构(业务决定架构)。