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