前言
前两日看了喵神的文章,具体地址在这里,讲的是如何用更加Swift的方式去写delegate。
文章写得非常的有帮助,层层递进的结构,很自然的给出最后改进版本。让你知其然,更知其所以然。
既然如此,何不来实践一下呢?
需求
在大部分的App结构中,比较常见的结构是
一个主模块下面有多个子模块,比方说,登陆模块,可以拆解开成Token恢复储存模块,Token验证模块,OAuth模块。
而其中的变化可以通过画状态图来表明。
继续用上面的例子来说明
- Authentication 主控制器控制Token储存器从Keychain中恢复出Token
- 若Token无法从Keychain中恢复出,告诉主控不可恢复且为何不可恢复,主控启动Oauth Manager进行重新授权
- 若Token可以恢复,则返回Token给主控,主控将Token传递给Verifier做验证
- 若验证成功,告诉主控Token有效,完成Authentication
- 若验证失败,告诉主控Token无效以及无效原因。主控启动Oauth模块进行重新授权
整个过程看起来非常符合逻辑。
可以抽象出的状态也不是很多,相对来说是个用状态机很不错的例子。
但是假设你不想写状态机,而就想通过简单的Protocol和Delegate来完成,那有没有什么好办法呢?
沿着这个思路,我们可以继续往下想。
实践
子模块
先来想想子模块应该怎么写。
看起来这里比较符合逻辑做法是,所有的子模块都因为遵守一个Protocol,这个Protocol里应该有的基础方法是Start()和finish()。
Start()做的事情是让子模块开始他内部的操作,而整体的控制器不需要知道子模块里到底做了什么,而只需要提供输入输出参数,完成之后由Delegate回调对应的方法。
那该如何定义呢?
Protocol需要有三样东西,delegate,Start()和finish()
Start()和finish()方法比较好理解,没啥特别的,但是对于delegate来说,我们并不能确定input和output的类型,那么,我们就需要暂时声明一个associatedtype来告诉编译器。
Associated Types
When defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are specified with the associatedtype keyword.
于是我们得到了这样的一个Protocol:
protocol AuthenticationProcedureProtocol {
associatedtype inputType
associatedtype outputType
var complete: Delegate { get set }
func start()
func finish()
}
然后我们让需要的那三个子模块都遵守这个Protocol,来看看效果
以其中的TokenReservoir为例
class TokenReservoir: AuthenticationProcedureProtocol {
var complete = Delegate()
func start() {
//Do the work
}
func finish() {
//Do the work, finish the job
complete.call("Token")
}
}
改进
这里你可能会说,哎,不对啊,你这样的岂不是又写死了,我希望我的start和finish都有对应的输入参数。
那该怎么做呢?
改Protocol很简单,因为我们还是不希望写死了类型,所以加入对应的associatedtype即可。
protocol AuthenticationProcedureProtocol {
associatedtype inputType
associatedtype outputType
var complete: Delegate { get set }
associatedtype startInputType
func start(_ start: startInputType)
associatedtype finishInputType
func finish(_ finish: finishInputType)
}
然后,我们需要去改对应的TokenReservoir了。
这个时候,突然觉得需要一个可以放置所有可能情况的Enumeration。
这个时候,你应该能想到,Result。
假设你的工程有引入任何一个Result(其中最出名的是这个)包,那么直接在其中写这样的一个结构就可以了
var delegate = Delegate, Void>()
比如这里,我们声明Result里需要返回的是String,或者是Error,Error定义在了AccessTokenVerificationError里面。
但是如果不想多引用一个包的话怎么写呢?
继续以TokenReservoir为例:
class TokenReservoir: AuthenticationProcedureProtocol {
enum Result {
enum FailureReason: String {
case noDataInKeychain = "noDataInKeychain"
case cannotRebuildModel = "cannotRebuildModel"
}
case success(String)
case failed(FailureReason)
}
var complete = Delegate()
func start(_ input: String) {
//Do the work
}
func finish(_ output: Result) {
//Do the work, finish the job
complete.call(output)
}
}
是不是看起来优雅多了?
那我们在调用的时候该怎么写呢?
class AuthenticationManager {
var verifier: TokenVerifier
var oauthManager: OauthManager
var tokenReservoir: TokenReservoir
// DI
init(verifier: TokenVerifier, oauthManager: OauthManager, tokenReservoir: TokenReservoir) {
self.verifier = verifier
self.oauthManager = oauthManager
self.tokenReservoir = tokenReservoir
}
func setupDelegate() {
// Token Verifier
verifier.complete.delegate(on: self) { (self, result) in
switch result {
case let .failed(failedReason):
print(failedReason.rawValue)
//Do something
case .success:
print("Successfully verified token")
//Do something
}
}
//Oauth Manager
oauthManager.complete.delegate(on: self) { (self, result) in
switch result {
case .failed:
print("Oauth failed")
//Do something
case .success:
print("Oauth succeeded")
//Do something
}
}
tokenReservoir.complete.delegate(on: self) { (self, result) in
switch result {
case let .failed(failedReason):
print(failedReason)
//Do something
case let .success(token):
print(token)
//Do something
}
}
}
}
这样就相对优雅的完成了delegate,同时,有点类似于接插件,之后如果需要再增加子模块的话,也会非常的容易。
总结
其实这只是我根据喵神文章中Delegate Pattern实践的一个结果,不能说一定是最好的,但是相对来说做到了比较合理的完成模块化的代码设计。
另外特别感谢一下喵神,毕竟其中代码还是从喵神的付费板块中来的,希望喵神不要介意。
你也可以从我的Github上找到Demo Project