Self-Manager?源于我們團隊內(nèi)部的黑話,“誒?你剛?cè)サ膭?chuàng)業(yè)公司有幾個 iOS 開發(fā)啊?” “就我一個” “靠,你這是 Self-Manager 啊”
最近,這個思路被我們當做了一種設(shè)計模式,即賦予一個 Widget 更大的權(quán)利,讓其自己負責自己的事件。
舉個簡單的栗子,這種負責展示頭像的視圖:
它的職責包括:
通過傳入的 URL,加載并展示頭像圖片
顯示一些附屬信息,比如大V的標志
將用戶點擊頭像的事件傳遞給外層的 View Controller 跳轉(zhuǎn)到用戶信息頁面
于是乎這個 Widget 的 API 可以長這個樣子:
@interfaceFDAvatarView:UIView
// 假設(shè) VIPInfo 是某個 Entity
- (void)configureWithAvatarURL:(NSURL*)URL VIPInfo:(id)info tapped:(void(^)(void))block;
@end
使用這個控件的人只需要調(diào)用這個 configure 方法就可以配置入?yún)⒑褪录幚怼5S之而來的就是一些蛋疼的問題:
configure 的調(diào)用者是 superview,上面的例子中也就是一個 UITableViewCell,但 Cell 這層并不知道自己的 ViewController 是誰,于是乎還得向上一級傳遞這個點擊事件,直到能獲取到 NavigationController,然后 Push 一個用戶信息的頁面。
這個 Avatar View 在 App 的各個地方都可能粗線,而且行為一致,那就意味著事件處理的 block,要散落在各個頁面中,同時也帶來了很多“只是為向上一層級轉(zhuǎn)發(fā)事件”的?“Middle Man”
為解決這個問題,就需要給這個 View 放權(quán),讓其自己 Handle 自己的事件,也就是?Self-Managed,為了不破壞 View 的純潔性,比較好的實踐是在 Category 中實現(xiàn):
@interfaceFDAvatarView(FDAvatarViewSelfManager)
- (void)selfManagedConfigureWithAvatarURL:(NSURL*)URL VIPInfo:(id)info uid:(NSString*)uid;
@end
實現(xiàn)時最好要調(diào)用 View 主類提供的 API:
@implementationFDAvatarView(FDAvatarViewSelfManager)
// 為后一個頁面的創(chuàng)建增加了個 UID 參數(shù)
- (void)selfManagedConfigureWithAvatarURL:(NSURL*)URL VIPInfo:(id)infoUID:(NSString*)UID{
[selfconfigureWithAvatarURL:URL VIPInfo:info tapped:^{
// 假設(shè) App 結(jié)構(gòu)是 Root -> TabBar -> Navigation -> ViewController
UITabBarController*tabBarControler = (id)[UIApplication.sharedApplication.delegate.window.rootViewController;
UINavigationController*navigationController = tabBarControler.selectedViewController;
// 創(chuàng)建用戶信息 View Controller
FDUserProfileViewController *profileViewController = [FDUserProfileViewController viewControllerWithUID:UID];
[navigationController pushViewController:profileViewController animated:YES];
? ? }];
}
@end
這里用到了類似 AOP 的思路,添加了對 App 層級的耦合,如果覺得這樣的耦合方式不妥的話,也可以封裝個全局方法去取到當前頂層的 Navigation Controller。
這樣,F(xiàn)DAvatarView 的調(diào)用者只需要配置入?yún)ⅲ溆嗟乃约喝芨愣耍词?App 內(nèi)很多處出現(xiàn)頭像,邏輯代碼也只有一份。
接下來再來個例子:
這個點贊的按鈕功能上有幾個職責:
顯示已有的點贊數(shù)
點擊按鈕后執(zhí)行一個小動畫,點贊數(shù) +1,同時發(fā)送網(wǎng)絡(luò)請求。
若已經(jīng)點贊,點擊執(zhí)行反向操作
若網(wǎng)絡(luò)請求發(fā)送失敗,則回退成點擊前的狀態(tài)
這個控件的 API 可以設(shè)計成這樣:
@interfaceFDLikeButton:UIButton
- (void)configureLikeStatus:(BOOL)likeOrNot count:(NSInteger)count animated:(BOOL)animated;
@end
因為繼承自 UIButton,所以外部可以直接設(shè)置其 action,就不增加 tappedHandler 的參數(shù)了。外部在點擊事件中需要調(diào)用這個配置方法,播放點贊動畫,緊接著發(fā)送一個網(wǎng)絡(luò)請求,若網(wǎng)絡(luò)請求失敗,可以再次調(diào)用這個 API 的無動畫版本回滾狀態(tài)。但像上一個例子一樣,網(wǎng)絡(luò)請求和事件處理邏輯相同,但代碼卻分部在各個頁面中,于是給這個 View 增加 Self-Managed 模式的 Category:
@interfaceFDLikeButton(FDLikeButtonSelfManager)
- (void)selfManagedConfigureWithLikeStatus:(BOOL)likeOrNot count:(NSInteger)count;
@end
偽代碼的實現(xiàn)如下:
@implementationFDLikeButton(FDLikeButtonSelfManager)
- (void)selfManagedConfigureWithLikeStatus:(BOOL)likeOrNot count:(NSInteger)count {
[selfconfigureLikeStatus:likeOrNot count:count animated:NO];
[selfaddTarget:selfaction:@selector(likeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)likeButtonTapped:(id)sender {
// +1 or -1 with animation
// Network request ^(NSError *error) {
//? ? if (error) {
//? ? ? ? rollback
//? ? }
// }
}
@end
記得面試題的那篇文章里還調(diào)侃說 “面試的時候聊聊設(shè)計、架構(gòu)挺好的,但別整出個往 UIButton 的子類里搞網(wǎng)絡(luò)請求的奇葩結(jié)構(gòu)就行”,結(jié)果就被自己打了個臉。不過從設(shè)計上,Self-Manager 模式并沒有破壞原有的 MVC 結(jié)構(gòu),上面兩個例子中的 View 依然可以不耦合具體業(yè)務(wù)邏輯的單拿出來用。使用 Category 的方式把應(yīng)該寫在 ViewController 中的代碼移動到 View 的文件中,讓功能更加的內(nèi)聚。
程序的復(fù)雜度并不會因哪種酷炫的設(shè)計模式所減少,能做到的只是對復(fù)雜度的切分和控制,即:
讓一大坨惡心的代碼變成幾小坨不那么惡心的代碼。
讓惡心的代碼只在一個地方惡心。
Self-Manager 模式我們實踐的時候?qū)懫饋砗荛_心,拋磚引玉一下,希望也能解決你的苦惱。