面向对象之设计模式
前言
要想推开架构师的那扇大门,就离不开设计模式这把钥匙。
对设计模式的理解与精通,是通往架构师之路的第一步
在任何面向对象语言的开发过程以及个人职业技能成长的道路中,新手与新手或者新手与高手的对决中,决定成败的往往是对知识点的纵向熟悉,和对知识点横向的涉猎
- 纵向:表示的对某一个知识点了解的有多深
- 横向:表示知识点面的广泛
而高手与高手的对决,决定成败的往往是对设计模式的理解与运用
- 遗憾的是,对设计模式的理解只可意会,不可言传。很难通过文字和讲解口头阐述,只能自身通过大量的代码和项目的积累产生自己的见解
- 这需要足够的兴趣和意志力。
技术,是一笔深情课
- 这需要足够的兴趣和意志力。
- 遗憾的是,对设计模式的理解只可意会,不可言传。很难通过文字和讲解口头阐述,只能自身通过大量的代码和项目的积累产生自己的见解
1.1-设计模式简介
1.1.1-什么是设计模式?
- 设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样
- 本人对设计模式的理解是:任何的设计模式的应用都离不开三点:代码的拓展性、可维护性、生存性
1.1.2-iOS有哪些设计模式?
- 本小节主要参考
《Objective-c编程之道 iOS设计模式解析》
一书 - iOS有21种设计模式
- 1.原型模式
- 2.工厂模式
- 3.抽象模式
- 4.生成器模式
- 5.单例模式
- 6.适配器模式
- 7.桥接模式
- 8.外观模式
- 9.中间者模式
- 10.观察者模式
- 11.组合模式
- 12.迭代器模式
- 13.访问者模式
- 14.装饰模式
- 15.责任链模式
- 16.模板模式
- 17.策略模式
- 18.命令模式
- 19.亨元模式
- 20.代理模式
- 21.备忘录模式
1.2-设计模式的六大原则
只需了解即可,课后感兴趣的可以慢慢咀嚼,设计模式的六大原则一个比一个难理解,一个比一个更深奥,单纯的靠文字和课堂讲解很难真正的掌握,必须要大量的代码积累和不断的思考感悟
描述设计模式的原则,这里笔者主要从三个角度分析
- 原则定义
- 原则由来
- 解决方案
***1.单一原则***
- 定义:一个类只负责一项职责
- 由来:如果类A的职责是完成功能P,类B的职责是完成功能Q。如果将功能Q的任何代码放到类A中,那么一旦类A发生变更,即使本该完成Q功能的类B代码没有任何变更,Q功能也会产生一定的故障。
- 解决方案:如果类A原本的职责是完成功能P,如果功能P在职责之外发生需求上的变更或者需要拓展,这个时候不可以在类A中修改代码,应该是新建一个类B来完成新增的功能Q
- 但是如果P功能是在职责之内发生变更,可以在类A中修改
***2.里氏替换原则***
- 定义:在程序P中,类B继承于类A,如果将P程序中的所有的类A的对象替换成类B的对象,这个程序不会发生任何的变化
- 由来:有一功能R,由类A完成。现需要将功能R进行扩展,扩展后的功能为Q,其中Q由原有功能R与新功能P组成。新功能P由类A的子类B来完成,则子类B在完成新功能P的同时,有可能会导致原有功能R发生故障
- 解决方案:子类继承父类的同时,遵循里氏替换原则,只在父类的基础上拓展功能,而避免重写或重载父类的方法,更改父类的功能。
- 在不更改父类原有的功能的基础上,可以酌情重写父类的方法在父类原有功能上进行拓展
- 所以问题的核心并不是
重写
,而是更改
- 所以问题的核心并不是
- 在不更改父类原有的功能的基础上,可以酌情重写父类的方法在父类原有功能上进行拓展
***3.依赖倒置原则***
- 定义:1.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。2.抽象不应该依赖于具体实现,具体实现应该依赖于抽象
- 依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
由来:耦合度太高导致类与类之间的依赖性太强
- 在类A调用类B的方法P,而方法P却依赖于类C,如果需求发生变更,类B中的方法需要依赖于类D,那么就必须要修改类B的代码,这样每一个变更都要修改类B的代码,而类B的逻辑比较多,属于高层模块,修改起来会导致其他一些问题。这主要是由于类B与类C之间的偶尔性太强导致。
- 假如有一个基类E,本身没有任何实现,只是指明了类E与类B之间的依赖关系,并且有一个接口。那么当类B需要依赖类C的时候,只需要指明类B依赖的类E的接口指向类C就可以。这样的话需求发生变更就不在需要修改类B的代码,甚至也不需要修改类E的代码,因为类E本身是一个虚拟类,没有任何的实现,我们只需要增加修改的代码放入类E的接口中即可。
- 在类A调用类B的方法P,而方法P却依赖于类C,如果需求发生变更,类B中的方法需要依赖于类D,那么就必须要修改类B的代码,这样每一个变更都要修改类B的代码,而类B的逻辑比较多,属于高层模块,修改起来会导致其他一些问题。这主要是由于类B与类C之间的偶尔性太强导致。
解决方案:解耦
- 低层模块尽量都要有抽象类或接口,或者两者都有。
- 变量的声明类型尽量是抽象类或接口。
- 使用继承时遵循里氏替换原则。
- 定义:1.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。2.抽象不应该依赖于具体实现,具体实现应该依赖于抽象
依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置
***4.接口隔离原则***
- 定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上
- 问题由来:在依赖倒置原则的基础上,有一个接口类A,类A有五个方法,分别是方法A1,A2,A3,A4,A5;其中类B通过方法A1,A2,A3依赖于类C,类D通过方法A4,A5,A6依赖于类E。那么对于B而言,接口类A的方法A4,A5是多余的,对于类D而言,接口类A的方法A1,A2,A3是多余的。结果本来好好的一个抽象接口类遵循了
依赖倒置原则
,结果对于类B和类D而言都存在臃肿的部分。 - 解决方案:将臃肿的接口类A,分离成独立的几个接口类,如类L,类M,类N,这三个接口类分别承担类A的原有的五个方法,这样的话类B和类D通过多个接口类去依赖对应的类,也就是
接口隔离原则
。
使用接口隔离原则需要注意以下几点:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则
接口隔离原则与OC的多重继承冲突的解决方案
接口隔离的原则实际上就是多重继承进行接口分离,但是OC不支持多重继承,所以在iOS开发中一般使用协议来进行接口分离,这就是为什么有的系统类同时遵循了好几个协议的原因
***5.迪米特法则***
- 定义:一个对象应该对其他对象保持最少的了解
- 问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
- 解决方案:尽量降低类与类之间的耦合
- 主要针对的是具体的实现类,在设计的时候应该在遵循其他原则的基础上最大限度的保证
高内聚
和低耦合
- 主要针对的是具体的实现类,在设计的时候应该在遵循其他原则的基础上最大限度的保证
自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的
- 迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
使用
迪米特法则
的时候可以联想一下公司的为人处事之道,假如你是员工,你的上面有主管,然后有总监,再上面有老板。当你有一件事只跟你的老板有关,那么如果你直接找你的老板,这就是属于越级的行为(与单一原则产生冲突)。而你先找你的主管,再通过主管找总监,最后通过总监找到老板,这个过程即复杂又让跟这件事没有关系的人知道了(与迪米特法则自身冲突),是不是突然变得非常棘手不知道怎么处理呢?所以有时候合理的理解设计模式需要强大的情商和悟性- 迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
- 迪米特法则最核心的内容是在设计类时,对类的耦合度和内聚度有一个合理的综合权衡,而并未单独的偏向一方
- 解决迪米特法则的冲突问题的时候其实可以考虑到21种设计模式中的某一些设计模式来中和某些冲突
- 比如使用中间人模式(又叫中介者模式)
- 解决迪米特法则的冲突问题的时候其实可以考虑到21种设计模式中的某一些设计模式来中和某些冲突
- 迪米特法则最核心的内容是在设计类时,对类的耦合度和内聚度有一个合理的综合权衡,而并未单独的偏向一方
- 迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
***6.开闭原则***
- 定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
- 由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试
- 解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
我曾经看过一些架构师的书籍,其中提到
我们进行设计的时候一定要遵守开闭原则
,当时觉得这句话说了等于没说。到今天回过头来在看一次,貌似又什么都说了。- 开闭原则是六大原则中最抽象和虚拟的原则
开闭原则 = 前面五大原则总和的平均分
- 开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行
- 也就是说前面的五大原则遵循好了,你自己而然就遵守了开闭原则,如果前面五个没有遵守好,也就没有遵守开闭原则。开闭原则是对前面五个原则的总结和约束!
- 开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行
***六大原则总结***
- 图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。
在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计
- 本小节参考书籍《设计模式之禅》《面向模式的软件架构》