iOS 与设计模式
iOS 与 设计模式
设计模式 (Design Pattern) 是一套被反复使用、多数人知晓、经过分类的、代码设计经验的总结。
我们使用设计模式的目的是为了提高代码可重用性、让代码更容易被理解、保证代码可靠性,使人们可以更加简单方便地复用成功的设计和体系结构。
设计原则
开闭原则
开闭原则就是说模块应该对扩展开放,而对修改关闭,即模块应该在尽量不修改原来代码的情况下进行扩展。
任何软件都需要面临一个很重要的问题,就是需求会随着时间的推移而发生变化。当软件系统需要面对新的需求时,应该尽量保证系统的设计框架是稳定的。如果一个软件符合开闭原则,那么就可以非常方便的对系统进行扩展并无需修改现有代码,使得系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,这就实现了在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
里氏代换原则
里氏代换原则严格表述是这样的 :如果每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 代换 o2 时,程序的 P 的行为没有变化,那么类型 S 就是 类型 T 的子类型。即所有引用父类的地方必须能透明地使用其子类的对象。
里氏代换原则是实现开闭原则的重要方式之一,由于使用父类对象的地方都可以使用子类对象,因此在程序中尽量使用父类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
里氏代换原则需要注意的问题 :
- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。为了保证系统的可扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
- 应该尽量把父类设计成抽象类或接口,让子类继承父类或实现接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例。
在依照里氏代换原则实现的软件中将一个父类对象替换成它的子类对象,程序将不会产生任何错误和异常,反之则不成立。
依赖倒置原则
依赖倒置原则,即抽象应该不依赖于细节,细节应该依赖于抽象,换言之,要针对接口编程,而不是针对实现编程。
依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类去做这些事情。为了确保该原则的应用,一个具体类应该只实现接口或抽象类中声明过的方法,而不要给出多余的方法。
在实现依赖倒置原则时,我们需要针对抽象层进行编程,而将具体类的对象通过依赖注入的方法注入到其它对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式包括 :构造注入、设值注入和接口注入。构造注入是通过构造函数来传入具体类的对象,设值注入是通过 setter 方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。
在大多数情况下,以上三种设计原则一同出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则时手段。
接口隔离原则
接口隔离原则,使用多个专门的接口,而不是使用单一的接口,即客户端不应该依赖那些它不需要的接口。每一个接口应该承担相对独立的角色,不干不该干的事情,该干的事情都要干。
这里的接口往往有两种不同的含义,一种是指一个类型所具有的方特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言的具体的接口定义,有严格的定义和结构。
- 当把接口理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可称为角色隔离原则。
- 如果把接口理解成狭义的特定语言的接口,那么接口隔离原则是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏,应该为客户端提供尽可能小的单独的接口。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的指责单一,需要将大接口中的方法根据其指责不同分别放在不同的小接口中,确保每个接口使用起来都较为方便。接口应该尽量细化,同时接口中的方法应该尽量少。
单一职责原则
单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小,即一个类只负责一个功能领域中的相应职责,或者说 :就一个类而言,应该只有一个引起它变化的原因。
单一职责原则是实现高内聚、低耦合的知道方针,它是最简单但又最难运用的原则。
最少知识法则
也叫迪米特法则,即一个软件实体应当尽可能少地与其它实体发生相互作用。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应该发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。
iOS 中的设计模式
单例模式
单例模式是一种常见的设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。
我们为什么需要单例模式呢?这是因为,对于系统中的某些类来说,只有一个实例很重要。比如如果不使用机制对窗口对象唯一化,将弹出多个窗口,如果这些窗口现实的内容完全一致,则是重复对象,浪费资源。如果这些窗口现实的内容不一致,则意味着某一瞬间系统有多个状态,与实际不符。
如何保证一个类只有一个实例且这个实例易于被访问呢?一般的解决方法是让类自身负责保存它的唯一实例。这个类可以保证没有其它实例被创建,并且它可以提供一个访问该实例的方法。
单例模式的优点
- 单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
- 因为类控制了实例化过程,所以类可以灵活更改实例化过程。
单例模式的缺点
- 虽然数量很少,但是每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。
- 可能的开发混淆。
- 不能解决删除单个对象的问题。
单例模式的代码实现
Objective-C :
1 | + (Singleton *)sharedInstance { |
Swift :
1 | class Singleton { |
代理模式
代理模式也是一种常见的设计模式,为其它对象提供一种代理以控制对这个对象的访问。
代理模式的优点 :
- 真实的角色就是实现实际的业务逻辑,不用关心其它非本职责的事物,通过后期的代理完成一件事物,附带的结果就是编程简洁清晰。
- 代理对象在客户端和目标对象之间起到中介作用
- 高扩展性
iOS 中的代理模式通过 @protocol 方式实现,用来传递事件或值。
1 | @protocol ObjectDelegate |
一般使用 weak 关键字来修饰代理属性,这是为了便面形成循环引用。
观察者模式
观察者模式也是 iOS 中常见的设计模式,观察者模式定义了一种一对多的依赖关系,一个或多个观察者对象同时监听某一个对象,当被观察者对象状态发生变化时,会通知所有观察者对象,至于观察者对象如何响应这个通知,则要看具体的实现。
在 iOS 中,观察者模式的实现具体表现为 :通知机制 (Notification) 和 KVO 机制 (Kev-Value Observing)
通知机制
被观察者达到某些触发条件后,发送通知给它的观察者对象。
注册通知接收者 :
1 | [[NSNotificationCenter defaultCenter] addObserver:self |
发布通知 :
1 | [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" |
在 iOS 9 之后,你不需要在观察者的销毁方法中注销观察者,但是在之前的 iOS 版本中,你必须在销毁方法中注销观察者。
KVO 机制
被观察的对象的属性变化时,发送通知给它的观察者对象。
KVO 机制的观察者对象观察的是对象的属性的变化,只有遵循 KVO 变更属性值的方式才会执行 KVO 的回调部分。
添加监听者 :
1 | [self.aProperty addObserver:self |
接收到属性变化通知时的回调 :
1 | - (void)observeValueForKeyPath:(NSString *)keyPath |
iOS 通过 Runtime 实现 KVO 机制 :
假设我们要观察对象 Computer 的 name 属性的变化,运行时 KVO 机制会动态的创建一个名为 NSKVONotifying_Computer 的新类,该类继承自 Computer 对象的本类,且 KVO 机制会重写 NSKVONotifying_Computer 的 name 属性的 setter 方法。这个重写的 setter 方法负责在调用 Computer 的 setter 方法的之前和之后,通知所有观察者 name 属性的更改情况。
在这个过程中,Computer 的 isa 指针被 KVO 机制修改为指向 NSKVONotifying_Computer 类,来实现当前类属性值改变的监听。
KVO 机制的键值观察通知依赖于 NSObject 的 willChangeValueForKey: 和 didChangeValueForKey: 两个方法。
工厂模式
工厂模式是常用的实例化对象模式,是用工厂方式代替 new 操作的一种模式。
简单工厂
简单工厂模式,又称静态工厂方法模式,是一种创建型模式,是由一个工厂对象决定创建出哪一种产品类的实例。
简单工厂的优缺点
- 优点 :简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。明确区分了各自的指责和权利,有利于整个软件体系结构的优化。
- 缺点 :工厂类集中了所有实例的创建逻辑,包含了过度的判断条件,维护起来不方便。
简单工厂的实现
1 | typedef NS_ENUM(NSInteger, InstanceType) { |
工厂方法
工厂方法模式,又称多态性工厂模式。在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂的角色,仅负责给出工厂子类必须实现的接口,而不负责哪一个产品类应当被实例化这种细节。
工厂方法模式就是简单工厂模式的衍生,解决了许多简单工厂模式的问题。它在基类中建立一个抽象方法,子类可以通过改写这个方法来改变创建对象的具体过程。工厂方法模式让子类来决定如何创建对象,来达到封装的目的。
工厂方法的优点
- 优点 :
- 子类提供挂钩。基类为工厂方法提供缺省实现,子类可以重写新的实现,也可以集成父类的实现,增加了灵活性。
- 屏蔽产品类。产品类的实现如何变化,调用者都不需要关心,只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不会发生变化。
- 典型的解耦框架。高层模块只需要知道产品的抽象类,其它的实现类都不需要关心。
- 多态性,客户代码可以做到与特定应用无关,适用于任何实体类。
抽象工厂模式
抽象工厂模式提供一个接口,用于创建一个对象家族,而无需指定具体类。工厂方法只涉及到创建一个对象的情况,有时候我们需要一族对象。
抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。根据里氏替换原则,任何接受父类型的地方,都应当能够接受子类型。因此,实际上系统所需要的,仅仅是类型与这些抽象产品角色相同的一些实例,而不是这些抽象产品的实例。换言之,也就是这些抽象产品的具体子类的实例。工厂类负责创建抽象产品的具体子类的实例。
抽象工厂的优点/缺点
- 优点:
- 抽象工厂模式隔离了具体类的生产,使得客户并不需要知道什么被创建。
- 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
- 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
- 缺点:增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。