说到设计模式,可能让人感到困惑。这里我引用 Christopher Alexander 大师的一句话:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。
在整个软件设计中,让人头大,让人感到困难的根本原因就是变化,而如何去解决各种变化带来的复杂性就是一个好的程序急需解决的。
学习设计模式,目的就是能够设计出更加优美,好维护,可复用,可拓展的优秀代码。利用复用来抵御变化。
将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象创建时具体类型实现引来的冲击。
通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象的结构带来冲击。
通过类继承或者对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
设计模式的六大原则有:
把这六个原则的首字母联合起来( L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计
下面我们来分别看一下这六大设计原则。
定义:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
循单一职责原的优点有:
定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展
问题由来:任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。因为变化,升级和维护等原因,如果需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试,所以当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现使我们需要的。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。
为什么使用开闭原则:
第一:开闭原则是最基础的设计原则,其它的五个设计原则都是开闭原则的具体形态,也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。开闭原则是抽象类,而其它的五个原则是具体的实现类。
第二:开闭原则可以提高复用性,在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来,而不是在一个类中独立实现一套业务逻辑。只有这样的代码才可以复用,逻辑粒度越小,被复用的可能性越大。为什么要复用呢?复用可以减少代码的重复,避免相同的逻辑分散在多个角落,减少维护人员的工作量以及系统变化时产生bug的机会。怎么才能提高复用率呢?设计者需要缩小逻辑粒度,直到一个逻辑不可以分为止。
第三:开闭原则可以提高维护性,一款软件量产后,维护人员的工作不仅仅对数据进行维护,还可能要对程序进行扩展,维护人员最乐意的事是扩展一个类,而不是修改一个类。
第四:面向对象开发的要求,万物皆对象,我们要把所有的事物抽象成对象,然后针对对象进行操作,但是万物皆发展变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到尽可能多变化的因素,然后留下接口,等待“可能”转变为“现实”。
如何使用开闭原则:
第一:抽象约束
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:
1.通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。
2.参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求
3.抽象层尽量保持稳定,一旦确定就不要修改
第二:元数据(metadata)控件模块行为
编程是一个很苦很累的活,那怎么才能减轻压力呢?答案是尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗的说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
第三:制定项目章程
在一个团队中,建立项目章程是非常重要的,因为章程是所有开发人员都必须遵守的约定,对项目来说,约定优于配置。这比通过接口或抽象类进行约束效率更高,而扩展性一点也没有减少
第四:封装变化
对变化封装包含两层含义:
(1)将相同的变化封装到一个接口或抽象类中
(2)将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。 封装变化,也就是受保护的变化,找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口。
定义:里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
由于面向对象语言的继承特性,子类拥有父类的所有方法,因此,将基类替换成具体的子类,子类也可以调用父类中的方法(其实是它自己的方法,继承于父类),但是如果要保证完全可以调用,光名称相同不行,还需要满足下面两个条件:
继承优点
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;提高代码的重用性;子类可以形似父类,但又异于父类;提高代码的可扩展性,实现父类的方法就可以“为所欲为”了;提高产品或项目的开放性。
继承缺点
继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;降低代码的灵活性。子类必须拥有父类的属性和方法;
增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果;大片的代码需要重构。
定义:迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学(Northeastern University)的一个名为迪米特(Demeter)的研究项目,由伊恩·荷兰(Ian Holland)提出,被 UML 创始者之一的布奇(Booch)普及,后来又因为在经典著作《程序员修炼之道》(The Pragmatic Programmer)提及而广为人知。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:
迪米特法则的优点:
迪米特法则的缺点:
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
定义 :使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
(1)单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
(2)单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
接口隔离原则的优点:
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。
(1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
(2)接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
(3)如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
(4)使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
(5)能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
**定义:**高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,其核心思想是:要面向接口编程,不要面向实现编程。
依赖倒置原则的作用
(1)依赖倒置原则可以降低类间的耦合性。
(2)依赖倒置原则可以提高系统的稳定性。
(3)依赖倒置原则可以减少并行开发引起的风险。
(4)依赖倒置原则可以提高代码的可读性和可维护性。
依赖倒置原则的实现方法
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
(1)每个类尽量提供接口或抽象类,或者两者都具备。
(2)变量的声明类型尽量是接口或者是抽象类。
(3)任何类都不应该从具体类派生。
(4)使用继承时尽量遵循里氏替换原则