國內業界你們對組件化的討論從今年年初開始到年尾,不外乎兩個方案:URL/protocol註冊調度,runtime調度。html
我以前批評過URL註冊調度是錯誤的組件化實施方案,在全部的基於URL註冊調度的方案中,存在兩個廣泛問題:git
其它各家的基於URL註冊的不一樣方案在這兩個廣泛問題上還有各類各樣的其餘問題,例如FRDIntent庫中的FRDIntent對象其本質是雞肋對象、原屬於響應者的業務被滲透到調用者的業務中、組件化實施方案的過程當中會產生對原有代碼的侵入式修改等問題。github
另外,我也發現仍是有人在都沒有理解清楚的前提下就作出了本身的解讀,流毒甚廣。我以前寫過關於CTMediator比較理論的描述,也有Demo,但唯獨沒有寫實踐方面的描述。我原本覺得Demo就足夠了,可如今看來仍是要給一篇實踐的文章的。swift
在更早以前,卓同窗的swift老司機羣裏也有人提出由於本身並無理解透徹CTMediator方案,因此不敢貿然直接在項目中應用。因此這篇文章的另外一個目的也是但願可以讓你們明白,基於CTMediator的組件化方案實施其實很是簡單,並且也是有章法可循的。這篇文章可能會去討論一些理論的東西,但主要還會是以實踐爲主。爭取作到可以讓你們看完文章以後就能夠直接在本身的項目中順利實施組件化。設計模式
最後,我但願這篇文章可以終結業界持續近一年的關於組件化方案的無謂討論和錯誤討論。xcode
我在github上開了一個orgnization,裏面有一個主工程:MainProject,咱們要針對這個工程來作組件化。組件化實施完畢以後的主工程就是ModulizedMainProject了。抽出來的獨立Pod、私有Pod源也都會放在這個orgnization中去。安全
在一個項目實施組件化方案以前,咱們須要作一個準備工做,創建本身的私有Pod源和快手工具腳本的配置:工具
pod repo add [私有Pod源倉庫名字] [私有Pod源的repo地址]
~/Project/MainProject
git clone git@github.com:casatwy/ConfigPrivatePod.git
source 'https://github.com/ModulizationDemo/PrivatePods.git'
改爲第一步裏面你本身的私有Pod源倉庫的repo地址PrivatePods
改爲第二步裏面你本身的私有Pod源倉庫的名字最後你的文件目錄結構應該是這樣:組件化
Project
├── ConfigPrivatePod
└── MainProject
到此爲止,準備工做就作好了。測試
MainProject是一個很是簡單的應用,一共就三個頁面。首頁push了AViewController,AViewController裏又push了BViewController。咱們能夠理解成這個工程由三個業務組成:首頁、A業務、B業務。
咱們這一次組件化的實施目標就是把A業務組件化出來,首頁和B業務都還放在主工程。
由於在實際狀況中,組件化是須要按部就班地實施的。尤爲是一些已經比較成熟的項目,業務會很是多,一時半會兒是不可能徹底組件化的。CTMediator方案在實施過程當中,對主工程業務的影響程度極小,並且是可以支持按部就班地改造方式的。這個我會在文章結尾作總結的時候提到。
既然要把A業務抽出來做爲組件,那麼咱們須要爲此作兩個私有Pod:A業務Pod(之後簡稱A Pod)、方便其餘人調用A業務的CTMediator category的Pod(之後簡稱A_Category Pod)。這裏多解釋一句:A_Category Pod本質上只是一個方便方法,它對A Pod不存在任何依賴。
此時你的文件目錄結構應該是這樣:
Project
├── ConfigPrivatePod
├── MainProject
└── A
而後cd到ConfigPrivatePod下,執行./config.sh
腳原本配置A這個私有Pod。腳本會問你要一些信息,Project Name
就是A,要跟你的A工程的目錄名一致。HTTPS Repo
、SSH Repo
網頁上都有,Home Page URL就填你A Repo網頁的URL就行了。
這個腳本是我寫來方便配置私有庫的腳本,pod lib create
也能夠用,可是它會直接從github上拉一個完整的模版工程下來,只是國內訪問github其實會比較慢,會影響效率。並且這個配置工做其實也不復雜,我就索性本身寫了個腳本。
這個腳本要求私有Pod的文件目錄要跟腳本所在目錄平級,也會在XCode工程的代碼目錄下新建一個跟項目同名的目錄。放在這個目錄下的代碼就會隨着Pod的發版而發出去,這個目錄之外的代碼就不會跟隨Pod的版本發佈而發佈,這樣子寫用於測試的代碼就比較方便。
而後咱們在主工程中,把屬於A業務的代碼拎出來,放到新建好的A工程的A文件夾裏去,而後拖放到A工程中。原來主工程裏面A業務的代碼直接刪掉,此時主工程和A工程編譯不過都是正常的,咱們會在第二步中解決主工程的編譯問題,第三步中解決A工程的編譯問題。
此時你的主工程應該就沒有A業務的代碼了,而後你的A工程應該是這樣:
A
├── A
| ├── A
| │ ├── AViewController.h
| │ └── AViewController.m
| ├── AppDelegate.h
| ├── AppDelegate.m
| ├── ViewController.h
| ├── ViewController.m
| └── main.m
└── A.xcodeproj
一樣的,咱們再建立A_Category,由於它也是個私有Pod,因此也照樣子跑一下config.sh
腳本去配置一下就行了。最後你的目錄結構應該是這樣的:
Project
├── A
│ ├── A
│ │ ├── A
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.m
│ │ ├── Assets.xcassets
│ │ ├── Info.plist
│ │ ├── ViewController.h
│ │ ├── ViewController.m
│ │ └── main.m
│ ├── A.podspec
│ ├── A.xcodeproj
│ ├── FILE_LICENSE
│ ├── Podfile
│ ├── readme.md
│ └── upload.sh
├── A_Category
│ ├── A_Category
│ │ ├── A_Category
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.m
│ │ ├── Info.plist
│ │ ├── ViewController.h
│ │ ├── ViewController.m
│ │ └── main.m
│ ├── A_Category.podspec
│ ├── A_Category.xcodeproj
│ ├── FILE_LICENSE
│ ├── Podfile
│ ├── readme.md
│ └── upload.sh
├── ConfigPrivatePod
│ ├── config.sh
│ └── templates
└── MainProject
├── FILE_LICENSE
├── MainProject
├── MainProject.xcodeproj
├── MainProject.xcworkspace
├── Podfile
├── Podfile.lock
├── Pods
└── readme.md
而後去A_Category下,在Podfile中添加一行pod "CTMediator"
,在podspec文件的後面添加s.dependency "CTMediator"
,而後執行pod install --verbose
。
接下來打開A_Category.xcworkspace
,把腳本生成的名爲A_Category
的空目錄拖放到Xcode對應的位置下,而後在這裏新建基於CTMediator的Category:CTMediator+A
。最後你的A_Category工程應該是這樣的:
A_Category
├── A_Category
| ├── A_Category
| │ ├── CTMediator+A.h
| │ └── CTMediator+A.m
| ├── AppDelegate.h
| ├── AppDelegate.m
| ├── ViewController.h
| └── ViewController.m
└── A_Category.xcodeproj
到這裏爲止,A工程和A_Category工程就準備好了。
去主工程的Podfile下添加pod "A_Category", :path => "../A_Category"
來本地引用A_Category。
而後編譯一下,說找不到AViewController
的頭文件。此時咱們把頭文件引用改爲#import <A_Category/CTMediator+A.h>
。
而後繼續編譯,說找不到AViewController
這個類型。看一下這裏是使用了AViewController
的地方,因而咱們在Development Pods
下找到CTMediator+A.h
,在裏面添加一個方法:
- (UIViewController *)A_aViewController;
再去CTMediator+A.m
中,補上這個方法的實現,把主工程中調用的語句做爲註釋放進去,未來寫Target-Action要用:
- (UIViewController *)A_aViewController { /* AViewController *viewController = [[AViewController alloc] init]; */ return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO]; }
補充說明一下,performTarget:@"A"
中給到的@"A"
實際上是Target對象的名字。通常來講,一個業務Pod只須要有一個Target就夠了,但一個Target下能夠有不少個Action。Action的名字也是能夠隨意命名的,只要到時候Target對象中可以給到對應的Action就能夠了。
關於Target-Action咱們會在第三步中去實現,如今不實現Target-Action是不影響主工程編譯的。
category裏面這麼寫就已經結束了,後面的實施過程當中就不會再改動到它了。
而後咱們把主工程調用AViewController
的地方改成基於CTMediator Category的實現:
UIViewController *viewController = [[CTMediator sharedInstance] A_aViewController]; [self.navigationController pushViewController:viewController animated:YES];
再編譯一下,編譯經過。
到此爲止主工程就改完了,如今跑主工程點擊這個按鈕跳不到A頁面是正常的,由於咱們尚未在A工程中實現Target-Action。
並且此時主工程中關於A業務的改動就所有結束了,後面的組件化實施過程當中,就不會再有針對A業務線對主工程的改動了。
此時咱們關掉全部XCode窗口。而後打開兩個工程:A_Category工程和A工程。
咱們在A工程中建立一個文件夾:Targets
,而後看到A_Category裏面有performTarget:@"A"
,因此咱們新建一個對象,叫作Target_A
。
而後又看到對應的Action是viewController
,因而在Target_A中新建一個方法:Action_viewController
。這個Target對象是這樣的:
頭文件:
#import <UIKit/UIKit.h> @interface Target_A : NSObject - (UIViewController *)Action_viewController:(NSDictionary *)params; @end 實現文件: #import "Target_A.h" #import "AViewController.h" @implementation Target_A - (UIViewController *)Action_viewController:(NSDictionary *)params { AViewController *viewController = [[AViewController alloc] init]; return viewController; } @end
這裏寫實現文件的時候,對照着以前在A_Category裏面的註釋去寫就能夠了。
由於Target對象處於A的命名域中,因此Target對象中能夠隨意import A業務線中的任何頭文件。
另外補充一點,Target對象的Action設計出來也不是僅僅用於返回ViewController實例的,它能夠用來執行各類屬於業務線自己的任務。例如上傳文件,轉碼等等各類任務其實均可以做爲一個Action來給外部調用,Action完成這些任務的時候,業務邏輯是能夠寫在Action方法裏面的。
換個角度說就是:Action具有調度業務線提供的任何對象和方法來完成本身的任務的能力。它的本質就是對外業務的一層服務化封裝。
如今咱們這個Action要完成的任務只是實例化一個ViewController並返回出去而已,根據上面的描述,Action能夠完成的任務其實能夠更加複雜。
而後咱們再繼續編譯A工程,發現找不到BViewController
。因爲咱們此次組件化實施的目的僅僅是將A業務線抽出來,BViewController
是屬於B業務線的,因此咱們不必把B業務也從主工程裏面抽出來。但爲了可以讓A工程編譯經過,咱們須要提供一個B_Category來使得A工程能夠調度到B,同時也可以編譯經過。
B_Category的建立步驟跟A_Category是同樣的,不外乎就是這幾步:新建Xcode工程、網頁新建Repo、跑腳本配置Repo、添加Category代碼。
B_Category添加好後,咱們一樣在A工程的Podfile中本地指過去,而後跟在主工程的時候同樣。
因此B_Category是這樣的:
頭文件:
#import <CTMediator/CTMediator.h> #import <UIKit/UIKit.h> @interface CTMediator (B) - (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText; @end 實現文件: #import "CTMediator+B.h" @implementation CTMediator (B) - (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText { /* BViewController *viewController = [[BViewController alloc] initWithContentText:@"hello, world!"]; */ NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"contentText"] = contentText; return [self performTarget:@"B" action:@"viewController" params:params shouldCacheTarget:NO]; } @end
而後咱們對應地在A工程中修改頭文件引用爲#import <B_Category/CTMediator+B.h>
,而且把調用的代碼改成:
UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"hello, world!"]; [self.navigationController pushViewController:viewController animated:YES];
此時再編譯一下,編譯經過了。注意哦,這裏A業務線跟B業務線就已經徹底解耦了,跟主工程就也已經徹底解耦了。
此時還有一個收尾工做是咱們給B業務線建立了Category,但沒有建立Target-Action。因此咱們要去主工程建立一個B業務線的Target-Action。建立的時候其實徹底不須要動到B業務線的代碼,只須要新增Target_B對象便可:
Target_B頭文件: #import <UIKit/UIKit.h> @interface Target_B : NSObject - (UIViewController *)Action_viewController:(NSDictionary *)params; @end Target_B實現文件: #import "Target_B.h" #import "BViewController.h" @implementation Target_B - (UIViewController *)Action_viewController:(NSDictionary *)params { NSString *contentText = params[@"contentText"]; BViewController *viewController = [[BViewController alloc] initWithContentText:contentText]; return viewController; } @end
這個Target對象在主工程內不存在任何侵入性,未來若是B要獨立成一個組件的話,把這個Target對象帶上就能夠了。
收尾工做就到此結束,咱們建立了三個私有Pod:A、A_Category、B_Category。
接下來咱們要作的事情就是給這三個私有Pod發版,發版以前去podspec裏面確認一下版本號和dependency。
Category的dependency是不須要填寫對應的業務線的,它應該是隻依賴一個CTMediator
就能夠了。其它業務線的dependency也是不須要依賴業務線的,只須要依賴業務線的Category。例如A業務線只須要依賴B_Category,而不須要依賴B業務線或主工程。
發版過程就是幾行命令:
git add .
git commit -m "版本號"
git tag 版本號
git push origin master --tags
./upload.sh
命令行cd進入到對應的項目中,而後執行以上命令就能夠了。
要注意的是,這裏的版本號
要和podspec文件中的s.version
給到的版本號一致。upload.sh
是配置私有Pod的腳本生成的,若是你這邊沒有upload.sh
這個文件,說明這個私有Pod你還沒用腳本配置過。
最後,全部的Pod發完版以後,咱們再把Podfile裏原來的本地引用改回正常引用,也就是把:path...
那一段從Podfile裏面去掉就行了,改動以後記得commit並push。
組件化實施就這麼三步,到此結束。
hard code
這個組件化方案的hard code僅存在於Target對象和Category方法中,影響面極小,並不會泄漏到主工程的業務代碼中,也不會泄漏到業務線的業務代碼中。
並且在實際組件化的實施中,也是依據category去作業務線的組件化的。因此先寫category裏的target名字,action名字,param參數,到後面在業務線組件中建立Target的時候,照着category裏面已經寫好的內容直接copy到Target對象中就確定不會出錯(僅Target對象,並不會牽扯到業務線自己原有的對象)。
若是要消除這一層hard code,那麼勢必就要引入一個第三方pod,而後target對象所在的業務線和category都要依賴這個pod。爲了消除這種影響面極小的hard code,並且只要按照章法來就不會出錯。爲此引入一個新的依賴,實際上是不划算的。
命名域問題
在這個實踐中,響應者的命名域並無泄漏到除了響應者之外的任何地方,這就帶來一個好處,遷移很是方便。
好比咱們的響應者是一個上傳組件。這個上傳組件若是要替換的話,只須要在它外面包一個Target-Action,就能夠直接拿來用了。並且包Target-Action的過程當中,不會產生任何侵入性的影響。
例如原來是你本身基於AFNetworking寫的上傳組件,如今用了七牛SDK上傳,那麼整個過程你只須要提供一個Target-Action封裝一下七牛的上傳操做便可。不須要改動七牛SDK的代碼,也不須要改動調用方的代碼。假若是基於URL註冊的調度,作這個事情就很蛋疼。
服務管理問題
因爲Target對象處於響應者的命名域中,Target對象就能夠對外提供除了頁面實例之外的各類Action。
並且,因爲其本質就是針對響應者對外業務邏輯的Action化封裝(其實就是服務化封裝),這就可以使得一個響應者對外提供了哪些Action(服務)
,Action(服務)的實現邏輯是什麼
獲得了很是好的管理,可以大大下降未來工程的維護成本。而後Category解決了服務應該怎麼調用
的問題。
但在基於URL註冊機制和Protocol共享機制的組件化方案中,因爲服務散落在響應者各處,服務管理就顯得十分困難。若是仍是執念於這樣的方案,你們只要拿上面提到的三個問題,對照着URL註冊機制和Protocol共享機制的組件化方案比對一下,就能明白了。
另外,若是這種方案把全部的服務歸攏到一個對象中來達到方便管理的目的的話,其本質就已經變成了Target-Action模式,Protocol共享機制其實就已經沒有存在乎義了。
高內聚
基於protocol共享機制的組件化方案致使響應者業務邏輯泄漏到了調用者業務邏輯中,並無作到高內聚
。
若是這部分業務在其餘地方也要使用,那麼代碼就要從新寫一遍。雖然它能夠提供一個業務高內聚的對象來符合這個protocol,但事實上這就又變成了Target-Action模式,protocol的存在乎義就也沒有了。
侵入性問題
正如你所見,CTMediator組件化方案的實施很是安全。由於它並不存在任何侵入性的代碼修改。
對於響應者來講,什麼代碼都不用改,只須要包一層Target-Action便可。例如本例中的B業務線做爲A業務的響應者時,不須要修改B業務的任何代碼。
對於調用者來講,只須要把調用方式換成CTMediator調用便可,其改動也不涉及原有的業務邏輯,因此是十分安全的。
另一個非侵入性的特徵體如今,基於CTMediator的組件化方案是能夠按部就班地實施的。這個方案的實施並不要求全部業務線都要被獨立出來成爲組件,實施過程也並不會修改未組件化的業務的代碼。
在獨立A業務線的過程當中若是涉及其它業務線(B業務線)的調用,就只須要給到Target對象便可,Target對象自己並不會對未組件化的業務線(B業務線)產生任何的修改。並且未來若是對應業務線須要被獨立出去的時候,也僅須要把Target對象一塊兒複製過去就能夠了。
但在基於URL註冊和protocol共享的組件化方案中,都必需要在未組件化的業務線中寫入註冊代碼和protocol聲明,並分配對應的URL和protocol到具體的業務對象上。這些其實都是沒必要要的,無故多出了額外維護成本。
註冊問題
CTMediator沒有任何註冊邏輯的代碼,避免了註冊文件的維護和管理。Category給到的方法很明確地告知了調用者應該如何調用。
例如B_Category給到的- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;
方法。這可以讓工程師一眼就可以明白使用方式,而沒必要抓瞎拿着URL再去翻文檔。
這能夠很大程度提升工做效率,同時下降維護成本。
實施組件化方案的時機
MVP階段事後,越早實施越好。
這裏說的MVP不是一種設計模式,而是最小价值產品的意思,它是產品演進的第一個階段。
通常來講天使輪就是用於MVP驗證的,在這個階段產品閉環還沒有肯定,所以產品自己的邏輯就會各類變化。可是過了天使輪以後,產品閉環已經肯定,此時就應當實施組件化,以應對A輪以後的產品拓張。
有的人說我如今項目很小,人也不多,因此不必實施組件化。確實,把一個小項目組件化以後,跟以前相比並無多大程度的改善,由於原本小項目就不復雜,改爲組件化以後,也不會更簡單。
但這實際上是一種很短視的認知。
組件化對於一個小項目而言,真正發揮優點的地方是在將來的半年甚至一年以後。
由於趁着人少項目小,實施組件化的成本就也很小,三四天就能夠實施完畢。因而等未來一年以後業務拓張到更大規模時,就不會束手束腳了。
但若是等到項目大了,人手多了再去實施組件化,那時候實施組件化的複雜度確定比如今規模還很小的時候的複雜度要大得多,三四天確定搞不定,並且實施過程還會很是艱辛。到那時你就後悔爲何當初沒有早早實施組件化了。
Swift工程怎麼辦?
其實只要Target對象繼承自NSObject就行了,而後帶上@objc(className)。action的參數名永遠只有一個,且名字須要固定爲params
,其它照舊。具體swift工程中target的寫法參見A_swift
由於Target對象是遊離於業務實現的,因此它去繼承NSObject徹底沒有任何問題。完整的SwiftDemo在這裏。
本文Demo
本文轉自:https://casatwy.com/modulization_in_action.html