Swift 面向协议编程
背景
Swift 面向协议编程在 WWDC 2015 中提出, 业界已经有很多优秀的实践, 比如 面向协议编程与 Cocoa 的邂逅. 项目中正在开发的功能模块主体依赖 OC 实现的 framework, 但是宿主项目已经迁移到 Swift, 因此不可避免的使用了类 OC 的处理方式, 比如使用单例模式实现模块的中心化和状态管理等. 在使用 Swift 为功能模块添加单元测试时, 发现项目中的大量单例实现使单元测试的编写异常艰难. 因此下文从更容易的为 Swift 编写单元测试的方向讲述.
解决方案
首先我们找了这两篇 OC 实现的参考: iOS 避免滥用单例, iOS 中的依赖注入. 经过调研之后, 决定对现有项目中已经实现的单例进行面向协议的方式进行改写, 然后通过构造器注入的方式将使用到的其他几个小模块注入到主功能模块中.
(1) 分离对象的定义和行为:
class Dog {
let name: String?
let birthPlace: String?
func bark() {
print("barking")
}
func eat() {
print("eating")
}
}
上面的类中, 想要对 Dog 的 bark() 和 eat() 进行测试, Swift 中没有 OC 中可以验证方法被调用的宏, 因此要验证这两个函数将会很困难. 在面向对象编程方式中, 鼓励将对象的定义(属性)和行为(函数)放在一个部分, 一起构成了对象. 在面向协议的编程方式中, 可以将对象的行为定义为一个抽象接口, 将对象的定义和行为分离, 这样在编写测试时可以只测试对象的行为.
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 协议方法中的实现即可配合验证条件进行测试.
(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) 使用工厂模式改造
抽离出来的协议越来越多, Manager 中的构造器越来越多,一些在子模块的子模块中使用的协议, 也得在 Manager 初始化时传递进来, 依赖关系有点混乱, 在编写单元测试时, 构造 mock Protocol 也十分繁琐.
1 明确什么方法可以定义在协议中
这一步主要是为了避免在被依赖对象中调用依赖对象的方法, 如果这个方法最终需要调用主模块的方法, 则会形成依赖循环, 此时的抽取协议方式应该不是很好的设计. 可以采取的方法有:
(1) 将控制权交还给 Manager, 使用侵入性低的比如通知的方式让 Manager 执行特定逻辑;
(2) 重新设计, 将这部分放在扩展中实现可能会更好;
2 明确模块或者协议之间的依赖关系
在 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 只和一个人(协议)打交道,只关心对象的使用, 不关心对象是如何生产的, 则模块之间的依赖将更易懂, 测试将会更加容易.
(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 协议, 并在 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 的引用关系, 不需要使用单例方式优化.
总结
回到我们遇到的问题:
(1) 单例设计的好处与坏处
好处: 状态共享, 一处定义多处使用
坏处: 披着羊皮的全局变量, 生命周期难以管理
解决方法:
避免单例的设计, 单例对象的的生命周期管理:
// 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. 将单例对象行为抽象出一个协议
2. 在使用的地方使用这个协议替换原来的单例对象
3. 在测试中, 使用 mock 对象遵守抽象出来的协议
(3) Swift 中依赖注入的一个最佳实践
如果被依赖的对象过多, 依赖关系不清晰, 可以尝试使用工厂模式隔离生产者和消费者.
网友评论