What's a Design Pattern

是解决软件设计中常见问题的可重用的设计思维,是能帮助我们写出容易理解和好用的代码的模版,是前辈们在开发过程中的总结经验。

设计模式可能不是一个需要专门学习的内容,当你用 Xcode 创建一个新的项目时,其实它已经为你创建好设计模式了:MVC, Delegate, Protocol, Singleton…。但在我看来学习设计模式是非常有益处的,好处自不多言。

本文要介绍的最常用的几种 Cocoa 设计模式,可以分为以下几种:

  • 创建型模式(Creational)
    • 单例模式(Singleton Pattern)
  • 结构型模式(Structural)
    • MVC 模式
    • 装饰模式(Decorator Pattern)
    • 适配器模式(Adapter Pattern)
    • 立面模式(Facade Pattern)
  • 行为型模式(Behavioral)
    • 观察者模式(Observer Pattern)
    • 备忘录模式(Memento Pattern)

I. MVC 设计模式

iOS 的核心设计模式,就不过多介绍了。在平常的开发中,要注意 model,view,view controller 之间各个模块的解藕合。

II. 单例模式(Singleton Pattern)

为文件的管理配置、和 API 的调用提供可全局访问的入口。

特点

  1. 应用单例模式的类只有一个实例,因此只存在一个访问该类的入口(同时保障了线程安全);
  2. 该实例(入口)是可全局访问、面向所有对象的。

实现

  1. 设置静态常量( shared )允许单例对象全局访问;
  2. 为其初始化器添加 private 关键字以防止在类外被实例化。
final class LibraryAPI {   
   static let shared = LibraryAPI()
   private init() {
      // ...
   }
}

Apple 官方用例: UserDefaults.standard , UIApplication.shared , UIScreen.main , FileManager.default

III. 立面模式(Facade Pattern)

有的地方也翻译作外观模式,但我更加偏好「立面」这个译法。

在一个 project 中,我们会用到各类的 API。比如在 Gins 这个项目里我需要用到 last.fm 的 API 获取音轨列表,用 Musixmatch 的 API 获取最新音轨的歌词摘要,另外,我们也需要一个类来单独处理对文件数据的操作。因此,一个集中统一的 API 机制是必要的。

优势

  1. 解藕代码。减少了外部代码对内部具体实现的依赖,接口内部实现的复杂性被隐藏起来,包括服务器的控制、数据库操作、文件系统和内存管理等;
  2. 逻辑简明。隐藏子系统的复杂性,对外部仅呈现一个简单的统一 API(unified API);
  3. 利于维护。日后当后端服务更换时,也不需要改变负责调用的部分,而只要更新立面内部的代码。

实现

  1. 在立面类中实例化负责各个 API 调用模块的类,private 修饰;
  2. 在立面类中调用各 API 类中的方法,这样该类就成了一个 unified API;
final class LibraryAPI {
   // add private instances
   private let persistencyManager = PersistencyManager()
   private let lastfmClient = LastfmClient()
   private let musixmatchClient = MusixmatchClient()

   // method calls..
}

与单例对象的关系

这时候回过去看,单例模式里创建的 LibraryAPI 同时又充当了立面类(unified interface)的角色。所以,这两种模式经常是相辅相成的。

为了使 API 的调用变得直观可控,使各个 API 类文件都只拥有唯一一个实例,并创建在一个单独的、可全局访问的类—— LibraryAPI 单例对象——中统一管理。

注意:虽然我们已经为子系统应用了立面模式,不过要明白的一点是,除非你是在建立分离的模块并且在使用访问控制,否则立面模式并没有保证客户端无法直接访问那些「隐藏」的类。

IV. 装饰模式(Decorator Pattern)

使用此模式允许向一个现有的对象添加新的功能,同时又不改变其结构。在 Swift 中,此模式有两种通用的应用:Extensions 和 Delegation。

优势

可以在为对象添加行为和功能的同时不改变其结构,它是类的继承的另一种替代选择。

实现一:Extensions

以 Model 类为例,我们可以为它扩展一个 description 属性,让它返回以特殊的形式字符串,这样就能很方便地作为 request body 给执行 post 请求的代码使用;

或者,如果想将 Model 属性以简单的 key-value 的形式在 table view 中展示,可以扩展一个 tableRepresentation 属性,使它返回一组二元组数组,每一个二元组就能对应到 table view 中的每一个单元格。

这种做法的巧妙之处在于,Model 对象原本是与数据在视图中如何展示无关的,但我们可以让它以更适合 View 对象的数据形式传递给后者,同时保持低耦合性。

注意:extensions 和通常定义下的装饰模式的定义还是有差别的——因为它不会实例化被扩展的类。另外,在 extensions 中不能重载方法。

实现二:Delegation

比较基础的东西,不多说。

UITableView 有两种代理类型:data source 和 delegate。通过前者,它能请求每个 section 要展示的行的数量,通过后者,能完成当一行被选中时需要做的事件。

V. 适配器模式(Adapter Pattern)

两个不兼容的接口之间的桥梁。在 Swift 中通过 protocol 来实现。

前面装饰模式提到,通过使扩展应用 delegate 和 data source,可以使 table view 完成数据的展示功能。实际上,这两种协议不是可选而是必须的,因为它们承担了衔接两个不兼容的对象的责任,使数据对象和视图对象能够配合完成工作,这便是最常见的适配器模式的栗子。

与装饰模式的关系

适配器(协议)对接兼容两个不同的类,于是类与类之间得以通过装饰模式(遵循协议)实现功能和职责的扩充,以使其能够一起工作。在 delegate 和 data source 的应用实例中,这两者是相辅相成的。

应用场景

想实现一个 iPod Coverflow 般的效果?Object library 中可没有叫做 HorizontalScrollView 的现成控件可供使用,那么我们就得继承一个 UIView 子类,并自行实现它与 Model 对象的协同工作,就像 table view 的 delegate 和 data source 一样。

VI. 观察者模式(Observer Pattern)

在观察者模式中,一个对象能够对另一个的对象的任何状态变化传递信息,同时能保持两个对象是解藕的(互相不建立引用关系)。

Cocoa 通过两种方式应用观察者模式:Notifications 和 Key-Value Observing (KVO)。

实现一:Notifications

一种订阅者(subscriber)-发布者(publisher)协调的机制,作为发布者的对象不需要知道任何订阅者的内部机制。发布者发布信息,接受者接收信息,并执行某特定函数。

Apple 官方用例:UIKeyboardWillShow/UIKeyboardWillHide :键盘的显示/隐藏;UIApplicationDidEnterBackground :后台的通知机制。

1 . 设置 notification key。Notification key 其实就是一组自定义的字符串,可以将它理解为一类特定通知的频道;

extension Notification.Name {
  static let mySpecialNotificationKey = Notification.Name("site.deans.specialNotificationKey")
}

2 . 设置发布者。在信息发布处添加 post(name:object:userInfo:) 方法。将信息载体以字典的形式通过 userInfo 发布,在第 3 步中被封装成 NSNotification 作为参数传入触发方法;

NotificationCenter.default.post(name: .mySpecialNotificationKey, object: self, userInfo: SOME_DICTIONARY)

3 . 设置订阅者。为接收的对象添加 addObserver(_:selector:name:object:) 。每当信息发布一次,就调用一次 SOME_METHOD(:) 方法;

NotificationCenter.default.addObserver(self, selector: #selector(SOME_METHOD(_:)), name: .mySpecialNotificationKey, object: nil)

4 . 定义触发方法。

实现二:Key-Value Observing (KVO)

使用 KVO 监测某一对象下特定属性的更改。

使用条件和限制:
  1. 被监测的属性所在的类应该是 KVO 服从(KVO compliant)的。必须继承于 NSObject
  2. 由于 Swift 默认禁用动态派发,因此还要将观测对象标记为 dynamic(使其在 Objective-C 运行时间动态派发)。
优势:
  1. 监视者可以对某一特定属性的任何变化接受通知,无论这一属性和观察者是否处在同一个类中。
  2. 可以灵活地针对一个或多个属性设定一个或多个观察器。(对于不同的值,可以为其单独设置独立的键)。
应用概要:
  1. 确保 KVO compliant。对于自定义的类,应继承自 NSObject + 属性应用 dynamic 修饰(像 UIImageView 则直接满足条件);
  2. 为独立的被观察属性设立独立的 key path 参数( \.objectToObserve.myDate );
  3. 若使用 completion closure pattern,当程序执行完该作用域将自动释放,否则应编写 removeObserver(_:forKeyPath:)不需使用后移除观察器。
class MyObjectToObserve: NSObject {
    @objc dynamic var myDate = NSDate()
    func updateDate() {
        myDate = NSDate()
    }
}

class MyObserver: NSObject {
    @objc var objectToObserve: MyObjectToObserve
    var observation: NSKeyValueObservation?
    
    init(object: MyObjectToObserve) {
        objectToObserve = object
        super.init()
        
        observation = observe(\.objectToObserve.myDate) { object, change in
            print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)")
        }
    }
}

class Implementation {  
    static let shared = Implementation()
    
    let observed = MyObjectToObserve()
    let observer: MyObserver
    
    private init() {
        observer = MyObserver(object: observed)
    }
}

Implementation.shared.observed.updateDate()

Swift 4 中,key path 有以下的形式:\<type>.<property>.<subproperty>

经常能通过编译器推断出来,但至少指定一个 是必要提供的。如, \.image\.UIImageView.image 的缩略写法。

但是,由于 KVC 依靠 Objective-C 逻辑支持,被观察的属性只能是继承自 NSObject ,这就是说结构体、枚举和泛型统统都不支持。考虑到这些历史遗留问题,寻找 KVO 的替代方案就成了一件很有必要的事情(ref 7)。

引用一句王巍的话:

KVO 是 Cocoa 中公认的最强大的特性之一,但是同时它也以烂到家的 API 和极其难用著称。

VII. 备忘录模式(Memento Pattern)

备忘录模式捕获一个对象的内部状态并将它外部化,简单说就是把内容存到别的地方。使用该模式可以记录用户的位置或浏览状态。

编/解码表示用户浏览状态的变量

1 . 在 iOS 中备忘录模式是状态恢复(State Restoration)的一部分,所以我们要先为需要启用状态恢复的视图控制器(或视图)分别设置好 Restoration ID

2 . 在 AppDelegate.swift 中添加以下代码,这样就启用了应用整体的状态恢复;

func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
  return true
}

func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
  return true
}

3 . 实现对某一个变量值的编/解码。定义静态字符串常量作为 restoration key,并重载方法;

override func encodeRestorableState(with coder: NSCoder) {
  coder.encode(VARIABLE, forKey: RESTORATION_KEY)
  super.encodeRestorableState(with: coder)
}

override func decodeRestorableState(with coder: NSCoder) {
  super.decodeRestorableState(with: coder)
  VARIABLE = coder.decodeInteger(forKey: RESTORATION_KEY)
  // do some updates with encoded variable..
}

数据的归档及序列化

如果想要为应用存储一些数据,最直接的方法是,遍历每一个 model 属性,保存至 plist 文件,在需要的时候再重新创建实例。

Archives and Serialization: Convert objects and values to and from property list, JSON, and other flat binary representations.

但以上方法有两个很大的局限:

  1. 当要存储的属性稍微复杂一点,代码就会很复杂。而且每当有新的类和属性需要存储,将要为其存取和加载编写新的代码;
  2. 你将不能存储私有属性;

归档及序列化机制(archiving and serialization)是苹果对备忘录模式的一种特有的实现。

在 Swift 4 之前,应用此机制的类需要继承 NSObject 并遵循 NSCoding 协议;Swift 4 开始,不论类、结构体还是枚举类型都能够运用此机制了。

实现步骤

1 . 声明该对象应用 Codable 协议;

2 . 创建并应用 encoder 实例;

private var documents: URL {
   return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
    
func encodeTracks() {
   let url = documents.appendingPathComponent(JSON_FILE_NAME)
   let encoder = JSONEncoder()
   guard let encodedData = try? encoder.encode(DATA_TO_ENCODE) else {
      return
   }
   try? encodedData.write(to: url)
}

3 . 创建并应用 decoder 实例。应用启动时,若编码文件存在,解码数据。

let savedURL = documents.appendingPathComponent(JSON_FILE_NAME)
let data = try? Data(contentsOf: savedURL)
if let existingData = data, let decoded = try? JSONDecoder().decode(TARGET_TYPE.self, from: existingData) {
   // assign decoded to private data property..
}

4 . 在每次要进入后台时调用第 2 步的 encodeTracks() 方法。为了达到这一目的,将 UIApplicationDelegate 下的实例方法 applicationWillResignActive(_:) 作为 notification key。

override func viewDidLoad() {       
   super.viewDidLoad()
   NotificationCenter.default.addObserver(self, selector: #selector(yourMethod(with:)), name: .UIApplicationWillResignActive, object: nil)
}

override func viewWillDisappear(_ animated: Bool) {
   super.viewWillDisappear(animated)     
   NotificationCenter.default.removeObserver(self, name: .UIApplicationWillResignActive, object: nil)
}

@objc func yourMethod(with notification: Notification) {
   // call encode method here..
}

完成这些工作后,你的应用就能够记住上一次打开时的状态了。运行项目后,在模拟器中按下 Home 键(此时编码器工作),重新启动项目(此时解码器工作)。


ref:

  1. Design Patterns on iOS using Swift - Part 1/2 - raywenderlich
  2. Design Patterns on iOS using Swift – Part 2/2 - raywenderlich
  3. 设计模式 - 菜鸟教程
  4. KVO - Swifter
  5. Key-Value Observing Programming Guide - Apple
  6. Is key-value observation (KVO) available in Swift? - Stack Overflow
  7. Exploring KVO alternatives with Swift - Scott Logic
Comments
Write a Comment