常见设计模式
App开发过程中,随着业务的不断发展,代码和逻辑不断增加,有时候不得不重构以前的代码,好的架构,利于代码的拓展和重构,下面就简单探讨一下
iOS
中常见的设计模式吧。
策略模式
模式动机
- 完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。
- 在软件开发中也常常遇到类似的情况,实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。
- 在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。
- 除了提供专门的查找算法类之外,还可以在客户端程序中直接包含算法代码,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。
- 为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。
模式定义
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。
策略模式是一种对象行为型模式。
策略模式结构
策略接口角色IStrategy:用来约束一系列具体的策略算法,策略上下文角色ConcreteStrategy使用此策略接口来调用具体的策略所实现的算法。
具体策略实现角色ConcreteStrategy:具体的策略实现,即具体的算法实现。
策略上下文角色StrategyContext:策略上下文,负责和具体的策略实现交互,通常策略上下文对象会持有一个真正的策略实现对象,策略上下文还可以让具体的策略实现从其中获取相关数据,回调策略上下文对象的方法。
策略模式解释
在讲策略模式之前,我们先看一个日常生活中的小例子:
现实生活中我们到商场买东西的时候,卖场往往根据不同的客户制定不同的报价策略,比如针对新客户不打折扣,针对老客户打9折,针对VIP客户打8折…
现在我们要做一个报价管理的模块,简要点就是要针对不同的客户,提供不同的折扣报价。
1 | package strategy.examp02; |
经过测试,上面的代码工作的很好,可是上面的代码是有问题的。上面存在的问题:把不同客户的报价的算法都放在了同一个方法里面,使得该方法很是庞大(现在是只是一个演示,所以看起来还不是很臃肿)。
下面看一下上面的改进,我们把不同客户的报价的算法都单独作为一个方法
1 | package strategy.examp02; |
针对我们一开始讲的报价管理的例子:我们可以应用策略模式对其进行改造,不同类型的客户有不同的折扣,我们可以将不同类型的客户的报价规则都封装为一个独立的算法,然后抽象出这些报价算法的公共接口
总结
总结:策略模式定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。
策略模式的本质: 分离算法,选择实现。
策略模式的优点:
1.策略模式的功能就是通过抽象、封装来定义一系列的算法,使得这些算法可以相互替换,所以为这些算法定义一个公共的接口,以约束这些算法的功能实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。
2.策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句。
3.扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了。
策略模式的缺点:
1.客户端必须了解所有的策略,清楚它们的不同:
如果由客户端来决定使用何种算法,那客户端必须知道所有的策略,清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。
2.增加了对象的数量:
由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。
3.只适合偏平的算法结构:
由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。
装饰者模式
Objective-C设计模式解析-装饰 - 小daniel的笔记本 - SegmentFault 思否
多用组合,少用继承。
动态给对象增加功能,从一个对象的外部来给对象添加功能,相当于改变了对象的外观,比用继承的方式更加的灵活。当使用装饰后,从外部系统的角度看,就不再是原来的那个对象了,而是使用一系列的装饰器装饰过后的对象。
结构
抽象构件角色“齐天大圣”接口定义了一个move()方法,这是所有的具体构件类和装饰类必须实现的。
1 | //大圣的尊号 |
具体构件角色“大圣本尊”猢狲类
1 | public class Monkey implements TheGreatestSage { |
抽象装饰角色“七十二变”
1 | public class Change implements TheGreatestSage { |
具体装饰角色“鱼儿”
1 | public class Fish extends Change { |
具体装饰角色“鸟儿”
1 | public class Bird extends Change { |
客户端调用
1 | public class Client { |
“大圣本尊”是ConcreteComponent类,而“鸟儿”、“鱼儿”是装饰类。要装饰的是“大圣本尊”,也即“猢狲”实例。
上面的例子中,第二种些方法:系统把大圣从一只猢狲装饰成了一只鸟儿(把鸟儿的功能加到了猢狲身上),然后又把鸟儿装饰成了一条鱼儿(把鱼儿的功能加到了猢狲+鸟儿身上,得到了猢狲+鸟儿+鱼儿)。
如上图所示,大圣的变化首先将鸟儿的功能附加到了猢狲身上,然后又将鱼儿的功能附加到猢狲+鸟儿身上。
高级模式
MVC
一个简单的举例
- 初期:依据
MVC
模式,把项目进行Model
、View
、Controller
简单分类:
- 中期:业务模块增加了,
Model
、View
、Controller
越来越多,于是,根据业务模块的分类,在每个模块内使用MVC
模式:
- 后期:
MVC
模式还是没有干净、很好地分割模块,在用户点击
、网络请求
和JSON解析数据
这些方面,会有交叉重叠的地方:
MVC-N
把项目分为四类:Model
、View
、Controller
、Networking
:
MVVM
上面MVC-N
新建了一个模块,来管理网络请求,然而,获取数据后的数据解析,还是放在了Controller
中,如何让Controller
专注于用户交互呢?而MVVM
模式,添加了ViewModel
来管理数据解析和网络请求等,解决了这个问题。
在MVVM
中,Controller
依然存在,但是不再直接持有Model
,Controller
持有ViewModel
,Model
被交给ViewModel
管理。
坦白说,有一部分逻辑确实是属于 controller
的,但是也有一部分逻辑是不应该被放置在 controller
中的。比如,将 model
中的 NSDate
转换成 view
可以展示的 NSString
等。在 MVVM
中,我们将这些逻辑统称为展示逻辑。
从上图中,我们可以非常清楚地看到 MVVM 中四个组件之间的关系。注:除了 view 、viewModel 和 model 之外,MVVM 中还有一个非常重要的隐含组件 binder :
- view :由 MVC 中的 view 和 controller 组成,负责 UI 的展示,绑定 viewModel 中的属性,触发 viewModel 中的命令;
- viewModel :从 MVC 的 controller 中抽取出来的展示逻辑,负责从 model 中获取 view 所需的数据,转换成 view 可以展示的数据,并暴露公开的属性和命令供 view 进行绑定;
- model :与 MVC 中的 model 一致,包括数据模型、访问数据库的操作和网络请求等;
- binder :在 MVVM 中,声明式的数据和命令绑定是一个隐含的约定,它可以让开发者非常方便地实现 view 和 viewModel 的同步,避免编写大量繁杂的样板化代码。在微软的 MVVM 实现中,使用的是一种被称为 XAML 的标记语言。
Multicast Closure DelegateNEW
代理模式大家应该最熟悉了,UIKit
中很多,经典的UITableViewDelegate
、UIAlertViewDelegate
. 基本原理是定一个协议Protocol
,列出需要实现的方法协议,然后交给指定的代理实现,可以有多个代理,为了避免循环引用,代理delegate
的属性设为weak
,
多播委托(MulticastDelegate)
多播委托(MulticastDelegate)继承自 `Delegate` ,表示多路广播委托;即,其调用列表中可以拥有多个元素的委托。实际上,我们自定义的委托的基类就是 `MulticastDelegate`。
假如现在有一个需求,我们以图片下载为例。这里先忽略哪些SDWebimage
等已经封装好的第三方类库。对于图片下载一般的过程如下:
- 先判断该图片
url
是否已经下载完毕。如果已经下载完毕那么直接回调显示图片。如果没有下载那么进入下载过程。 - 使用合适的图片下载器下载图片。
- 图片下载完毕后回调显示图片。并且把该图片存到缓存中。
这里的难点是回调。如果一个页面中有多个地方需要显示同一张图片,那么势必会发生这样一种情况,就是同时有多个请求下载同意url的图片,并且下载完成后需要同时在多个地方显示图片。要是实现这样的需求,用现有的方案貌似很难解决。有的同学会想到通知中心,但是通知中心其实是一个广播服务,只要注册了接受该通知那么所有的注册者都能收到通知,但事实上我只需要在我需要下载的那个url的图片下载完后给出通知,而不需要所有的下载完毕事件都通知。这时候我们就需要多播委托了。
普通的delegate
只能是一对一的回调,无法做到一对多的回调。而多播委托正式对delegate
的一种扩展和延伸,多了一个注册和取消注册的过程,任何需要回调的对象都必须先注册。比较经典的就是XMPPframework
这个框架,用了很多多播委托模式(GCDMulticastDelegate
)。
多播闭包委托
多播闭包委托 (Multicast Closure Delegate)继承自多播委托。
六大设计原则
单一职责原则
单一职责的定义是:应该有且仅有一个原因引起类的变更。 单一职责原则有什么好处:
类的辅助性降低,实现什么职责都有清晰明确的定义;
可读行提高,复杂性降低,那当然可读性提高了;
可维护性提高,可读性提高,那当然更容易维护;
变更引起的风险降低,变更是必不可少的,如果接口的单一职责做的好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
里氏替换原则
面向对象的语言,继承的好处:
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
提高代码的重用性;
子类可以形似父类,但又异于父类;
提高代码的可扩展性;
提高产品或项目的开发性;
坏处:
继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
增强了耦合性。当父类的常量、变量和方法修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果–大段代码需要重构。
为了让继承的好处大于坏处,引入了里氏替换原则(Liskov Substitution Principle,LSP):所有引入基类的地方必须能够透明的使用其子类对象。 通俗的讲,只要父类出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
子类可以扩展父类的功能,但不能改变父类原有的功能。
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
子类中可以增加增加特有的方法;
里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。
子类必须完全实现父类的方法;
在类中调用其他类时,务必使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了 LSP 原则。
如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生”畸变”,则建议断开继承关系,采用依赖、聚集、组合等关系代替继承。
子类可以有自己的个性;
覆盖或实现父类的方法时输入参数可以被放大;
覆写或实现父类的方法时输出结果可以被缩小;
依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP)
包含三层含义:
高层模块不应该依赖低层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象;
接口隔离原则
接口可以分为两种:
实例接口;
类接口;
接口隔离原则是对接口进行规范约束,其包含以下层含义:
接口要尽量小;
接口要高内聚;
定制服务;
接口设计是有限度的;
迪米特法制
迪米特法则(Law of Demter,LoD),也称为最少知识原则(Least Knowledge Priciple,LKP),虽然名字不同,但描述的是一同一个规则:一个对象应该对其他对象有最少的了解。
开闭原则
- 开闭原则:一个软件实体如类、模块和函数应该对扩展开发,对修改关闭。
- 开闭原则是最基础的一个原则,开闭原则在面向对象设计领域中的地位就类似于牛顿第一定律在力学、勾股定理在几何学,质能方程在狭义相对论中的地位,其地位无人能及。
开闭原则是通过以下几个方面来理解其重要性。
- 开闭原则对测试的影响;
- 开闭原则可以提高复用性;
- 开闭原则可以提高可维护性;
- 面向对象开发的要求;
- 如何使用开闭原则
抽象约束;
- 元数据(metadata)控制模块行为;
- 制定项目章程;
- 封装变化;
- 将相同的变化封装到一个接口或者抽象中;
- 将不同变化封装在不同的接口或者抽象中;
[附]设计原则
总结
个人觉得,一种设计模式代表的是一种思想。平时开发的过程中,尽量根据业务需求和已有的代码结构,参考成熟的设计模式,选取最适合当前的需求的模式,而这些优秀的模式也是建立在不断的Code Review
和对对代码的Deep Thinking
的基础上,不断优化的成果,值得借鉴。
软件设计最大的难题就是应对需求的变化,但是纷繁复杂的需求变化又是不可预测的。我们要为不可预测的事情做好准备。
Single Responsibility Principle: 单一职责原则;
Open Closed Principle:开闭原则;
Liskov Substitution Principle:里氏替换原则;
Law of Demeter:迪米特法则;
Interface Segregation Principle:接口隔离原则;
Dependence Inversion Principle:依赖倒置原则;
把这6个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(solid,稳定的)其代表的含义也就是把这6个原则结合使用的好处:建立稳定、灵活、健壮的设计,而开闭原则又是重中之重,是最基础的原则,是其他5大原则的精神领袖。我们在使用开闭原则是要注意以下几个问题。
开闭原则也只是一个原则; 开闭原则只是精神口号,实现拥抱变化的方法非常多,并不局限于这六大设计原则,但是遵循这个六大设计原则基本上可以应对大多数变化。
项目规章非常重要; 如果你是一位项目经理或架构师,应尽量让自己的项目成员稳定,稳定后才能建立高效的团队文化,章程是一个团队所有成员共同的知识结晶,也是所有成员必须遵守的约定。优秀的章程能带给项目非常多的好处,如提高开发效率,降低缺陷率、提高团队士气、提高技术成员水平,等等。
预知变化 在实现过程中国,架构师或项目经理一旦发现有变化的可能,或者变化曾经发生过,则需要考虑现有的架构是否可以轻松地实现这一变化,架构师设计一套系统不仅要符合现有的需求,还要适应可能发生的变化,这才是一个优良的架构。
开边原则是一个终极目标,任何人包括大师级人物都无法百分之百做到,但朝这个方向努力,可以非常显著的改善一个系统的架构,真正做到“拥抱变化”。