iOS 基础知识:设计模式


概念理解

iOS 有哪些常见的设计模式?

单例模式

单例保证了应用程序的生命周期内仅有一个该类的实例对象,而且易于外界访问。UIApplication、NSBundle、NSNotificationCenter、NSFileManager、NSUserDefault、NSURLCache 等都是单例。

代理模式

代理 Delegate 是协议的一种,通过 @protocol 方式实现,常见的有 tableView、textField 等。

观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个对象。在 iOS 中,观察者模式的具体实现有两种:通知机制(notification)和 KVO 机制(Key-value Observing)

工厂模式

见下文。

单例会有什么优缺点?

优点

  1. 提供了对唯一实例的受控访问。
  2. 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  3. 允许可变数目的实例。

缺点

  1. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  2. 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  3. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

编程中的六大设计原则?

1. 单一职责原则

通俗地讲就是一个类只做一件事。例如:CALayer 负责动画和视图的显示;UIView 只负责事件传递、事件响应。

2. 开闭原则

对修改关闭,对扩展开放。 要考虑到后续的扩展性,而不是在原有的基础上来回修改。

3. 接口隔离原则

使用多个专门的协议、而不是一个庞大臃肿的协议,如 UITableViewDataSource + UITableviewDelegate。

4. 依赖倒置原则

抽象不应该依赖于具体实现、具体实现可以依赖于抽象。调用接口感觉不到内部是如何操作的。

5. 里氏替换原则

父类可以被子类无缝替换,且原有的功能不受任何影响,如:KVO。

6. 迪米特法则

一个对象应当对其他对象尽可能少的了解,实现高聚合、低耦合。

工厂模式

什么是工厂模式?

工厂模式属于创建型模式,具体可以分为简单工厂模式、工厂模式和抽象工厂模式。

简单工厂模式

简单工厂模式,定义一个工厂类,根据传入参数的不同返回不同的实例,被创建的实例具有共同的父类或者接口。

工厂模式

工厂模式,定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式是简单工厂的进一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。

抽象工厂模式

抽象工厂模式,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。(在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的对象)。抽象工厂模式是工厂模式的进一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象。这是和工厂方法最大的不同点。

工厂模式有什么意义?

  • 延迟及隐藏子类实例化过程。
  • 解耦,把对象的创建和使用的过程分开。
  • 代码复用,简化实例化代码。
  • 容易扩展或修改。

工厂模式的使用场景?

  • 对象的创建过程、实例化准备工作很复杂,需要初始化很多参数、查询数据库等。
  • 类本身有很多子类,这些类的创建过程在业务中容易发生改变,或者对类的调用容易发生改变。

可以看到,抽象工厂方法隐藏了具体工厂类创建具体产品子类实现的过程,只暴露了抽象工厂的创建接口。结合到使用类簇的原因,我们就会发现这完美解决了我们的需求——只暴露抽象类及创建接口,隐藏一系列子类的实例化过程及具体实现,简化 API。

类簇

谈谈你对类簇的理解

类簇是 Foundation 框架广泛使用的设计模式。类簇在公共抽象父类下对多个私有的具体子类进行分组。以这种方式对类进行分组简化了面向对象框架的公共可见体系结构,而不会降低其功能丰富度。类簇是基于抽象工厂设计模式的。

为什么苹果要这样设计呢?

以NSArray为例,为了保持数组存取的高效,针对不同情况(可变、不可变、单元素等情况)必然要有相应的子类来优化实现。如果全部都用可见子类来实现的话,那么对于程序员来说,就要熟知大量的子类及其API,并且在调用的时候也要分情况去调用,这样使用起来太复杂了。而且如果子类实现改变的话,有可能导致接口也改变,框架API变化也就更加频繁,不利于使用。

为了解决这个问题,NSArray和NSMutableArray作为公开抽象父类,抽象了array功能的接口,但是具体的实现则是通过私有的具体子类来实现。再结合抽象工厂设计模式,程序员就可以通过抽象父类引用而指向私有具体子类,由子类根据自身情况实现父类抽象的方法。这样接口十分简洁,框架底层子类变化时也不会影响到接口的变化,增强了接口稳定性。

OC 中有哪些类簇呢?

NSData、NSArray、NSDictionary、NSString、NSNumber 等都是类簇。日常开发 debug 过程中我们可能会发现 _NSCFString__NSArrayI 这样的类,其实这就是其类簇下面的私有子类。

如何子类化类簇?

类簇通过抽象工厂模式实现,那么如果我们要写一个子类继承自 NSArray,为了实现该子类的实例化,按照类簇实现思路,我们必须增加该子类在工厂中实例化过程。但是我们仔细思考一下,这样是不可能做到的。因此应该尽量避免使用类簇来创建新的子类,如果必须这样做则必须足够的小心。下面讲一讲我们如何子类化类簇。
根据官方文档《Concepts in Objective-C Programming》,如果要子类化类簇,要做到:

  • 以公共抽象类为父类,比如 NSNumber、NSArray 等,而非其子类
  • 声明必要的变量,并提供自定义存储
  • 重写(覆盖)父类所有初始化方法
  • 重写父类中原始方法

首先,类簇的父类都是抽象父类(Abstract Classes),类簇只有抽象父类是公开可见的,因此我们也只能以公共抽象类作为父类。子类继承了父类的接口,但是不包括实例变量(抽象父类也不会声明实例变量)。所以子类必须声明自己需要的实例变量,并且定义其存储。其次,由于抽象父类并没有直接实现实例化过程,因此子类必须自己重写父类所有的初始化方法。最后,原始方法(primitive methods)是构成类的基本接口,其他方法可以通过原始方法派生而来(也叫做派生方法 derived methods)。以 NSArray 为例,其原始方法包括:countobjectAtIndex 这两个,因此其子类也必须重写这两个原始方法。一般而言在 Foudation 中,在注释中包含 primitive 或者在 NSArray 中声明的是原始方法,但是在分类中,比如 NSArray(NSExtendedArray)声明的是派生方法。

在子类化类簇过程中,父类的 alloc 如果没有相对应的子类调用的是 [super alloc],即 NSObject 的 alloc,父类的 init 没有具体实现。因此对于子类,可以不重写 alloc,但是对于有自己声明变量的必须要重写 init 。除了重写 init 之外,也可以根据需要提供 + className 类方法和实现。