Swift 面向協(xié)議編程
背景
Swift 面向協(xié)議編程在 WWDC 2015 中提出, 業(yè)界已經有很多優(yōu)秀的實踐, 比如 面向協(xié)議編程與 Cocoa 的邂逅. 項目中正在開發(fā)的功能模塊主體依賴 OC 實現的 framework, 但是宿主項目已經遷移到 Swift, 因此不可避免的使用了類 OC 的處理方式, 比如使用單例模式實現模塊的中心化和狀態(tài)管理等. 在使用 Swift 為功能模塊添加單元測試時, 發(fā)現項目中的大量單例實現使單元測試的編寫異常艱難. 因此下文從更容易的為 Swift 編寫單元測試的方向講述.
解決方案
首先我們找了這兩篇 OC 實現的參考: iOS 避免濫用單例, iOS 中的依賴注入. 經過調研之后, 決定對現有項目中已經實現的單例進行面向協(xié)議的方式進行改寫, 然后通過構造器注入的方式將使用到的其他幾個小模塊注入到主功能模塊中.
(1) 分離對象的定義和行為:
class Dog {
let name: String?
let birthPlace: String?
func bark() {
print("barking")
}
func eat() {
print("eating")
}
}
上面的類中, 想要對 Dog 的 bark() 和 eat() 進行測試, Swift 中沒有 OC 中可以驗證方法被調用的宏, 因此要驗證這兩個函數將會很困難. 在面向對象編程方式中, 鼓勵將對象的定義(屬性)和行為(函數)放在一個部分, 一起構成了對象. 在面向協(xié)議的編程方式中, 可以將對象的行為定義為一個抽象接口, 將對象的定義和行為分離, 這樣在編寫測試時可以只測試對象的行為.
protocol DogCategory {
func bark()
func eat()
}
class Dog {
let name: String?
let birthPlace: String?
}
extension Dog: DogCategory {
func bark() {
print("barking")
}
func eat() {
print("eating")
}
}
測試時使用一個 mock 對象來實現 DogCategory 協(xié)議方法中的實現即可配合驗證條件進行測試.
(2) 各種依賴注入方式
1 構造器注入
protocol FileManager {
...
}
class MessageLoader {
private let fileManager: FileManager
private let cache: Cache
init(fileManager: FileManager = .default,
cache: Cache = .init()) {
self.fileManager = fileManager
self.cache = cache
}
}
2 屬性注入
class MessageViewController: UIViewController {
var loader: MessageLoader = MessageLoader.shared()
var engine = NetworkEngine()
}
3 方法注入
class MessageManager {
func loadMessages(matching query: String,
completionHandler: @escaping ([Message]) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let database = self.loadDatabase()
let messages = database.filter { message in
return message.matches(query: query)
}
completionHandler(messages)
}
}
}
(3) 使用工廠模式改造
抽離出來的協(xié)議越來越多, Manager 中的構造器越來越多,一些在子模塊的子模塊中使用的協(xié)議, 也得在 Manager 初始化時傳遞進來, 依賴關系有點混亂, 在編寫單元測試時, 構造 mock Protocol 也十分繁瑣.
1 明確什么方法可以定義在協(xié)議中
這一步主要是為了避免在被依賴對象中調用依賴對象的方法, 如果這個方法最終需要調用主模塊的方法, 則會形成依賴循環(huán), 此時的抽取協(xié)議方式應該不是很好的設計. 可以采取的方法有:
(1) 將控制權交還給 Manager, 使用侵入性低的比如通知的方式讓 Manager 執(zhí)行特定邏輯;
(2) 重新設計, 將這部分放在擴展中實現可能會更好;
2 明確模塊或者協(xié)議之間的依賴關系
在 OC 中, 如果多個模塊共同依賴了一個模塊, 最可能的做法便是將這個共同模塊改寫成單例, 在其他模塊中直接調用. 但是在 Swift 中, 應該盡量避免單例設計.
舉例:
class MessageListViewController: UITableViewController {
private let loader: MessageLoader
init(loader: MessageLoader) {
self.loader = loader
super.init(nibName: nil, bundle: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loader.load { [weak self] messages in
self?.reloadTableView(with: messages)
}
}
}
// need a message sender
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// options: get a singleton messageSender
// let sender = MessageSender.shared
let message = messages[indexPath.row]
let viewController = MessageViewController(message: message, sender: sender)
navigationController?.pushViewController(viewController, animated: true)
}
在點擊 MessageListViewController 時, 需要將 messageSender 傳遞給 MessageViewController. 除了使用單例獲取 MessageSender(盡量避免單例設計), 在 MessageListViewController 中使用構造器傳入...
// if just use MessageListViewController for showing, we don't need know sender and others
class MessageListViewController: UITableViewController {
init(loader: MessageLoader, sender: MessageSender, logger: MessageLogger, ...) {
...
}
}
如果能將這些依賴都歸置在一個地方管理, 讓 Manager 只和一個人(協(xié)議)打交道,只關心對象的使用, 不關心對象是如何生產的, 則模塊之間的依賴將更易懂, 測試將會更加容易.
(1) 將所有依賴挪到一個 Container 中, 讓 Manager 只依賴 Container.
class DependencyContainer {
private lazy var messageSender = MessageSender(networkManager: networkManager)
private lazy var messageLoader = MessageLoader(networkManager: networkManager)
}
class MessageListViewController: UITableViewController {
private let factory: DependencyContainer
init(factory: DependencyContainer) {
self.factory = factory
super.init(nibName: nil, bundle: nil)
}
}
(2) 定義 ViewControllerFactory, LoaderFactory, SenderFactory 協(xié)議, 并在 Container 中實現.
protocol ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController
func makeMessageViewController(for message: Message) -> MessageViewController
}
protocol MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader
}
extension DependencyContainer: ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController {
return MessageListViewController(factory: self)
}
func makeMessageViewController(for message: Message) -> MessageViewController {
return MessageViewController(message: message, sender: messageSender)
}
}
extension DependencyContainer: MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader {
return MessageLoader(networkManager: networkManager)
}
}
(3) 使用方法
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// creating MessageListViewController
let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()
window.rootViewController = UINavigationController(
rootViewController: listViewController
)
return true
}
class MessageListViewController: UITableViewController {
private let container: DependencyContainer
private lazy var loader = container.makeMessageListViewController()
init(container: DependencyContainer) {
self.container = container
super.init(nibName: nil, bundle: nil)
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let message = messages[indexPath.row]
let viewController = self.container.makeMessageViewController(for: message)
navigationController?.pushViewController(viewController, animated: true)
}
這里需要使用工廠的對象會擁有對 Container 的引用關系, 不需要使用單例方式優(yōu)化.
總結
回到我們遇到的問題:
(1) 單例設計的好處與壞處
好處: 狀態(tài)共享, 一處定義多處使用
壞處: 披著羊皮的全局變量, 生命周期難以管理
解決方法:
避免單例的設計, 單例對象的的生命周期管理:
// a weak singleton in OC
+ (id)sharedInstance {
static __weak ASingletonClass *instance;
ASingletonClass *strongInstance = instance;
@synchronized(self) {
if (strongInstance == nil) {
strongInstance = [[[self class] alloc] init];
instance = strongInstance;
}
}
return strongInstance;
}
// a weak singleton in Swift
class SharedResource {
static weak var weakInstance: Resource?
static var sharedInstance: Resource {
get {
if let instance = weakInstance {
return instance
} else {
let newInstance = Resource()
weakInstance = newInstance
return newInstance
}
}
}
}
(2) Swift 中怎么對使用單例設計的對象進行測試
1. 將單例對象行為抽象出一個協(xié)議
2. 在使用的地方使用這個協(xié)議替換原來的單例對象
3. 在測試中, 使用 mock 對象遵守抽象出來的協(xié)議
(3) Swift 中依賴注入的一個最佳實踐
如果被依賴的對象過多, 依賴關系不清晰, 可以嘗試使用工廠模式隔離生產者和消費者.