在現有工程中實施基於CTMediator的組件化方案


國內業界大家對組件化的討論從今年年初開始到年尾,不外乎兩個方案:URL/protocol注冊調度,runtime調度。

 

我之前批評過URL注冊調度是錯誤的組件化實施方案,在所有的基於URL注冊調度的方案中,存在兩個普遍問題:

 

  1. 命名域滲透
  2. 因注冊是不必要的,而帶來同樣不必要的注冊列表維護成本

 

其它各家的基於URL注冊的不同方案在這兩個普遍問題上還有各種各樣的其他問題,例如FRDIntent庫中的FRDIntent對象其本質是雞肋對象、原屬於響應者的業務被滲透到調用者的業務中、組件化實施方案的過程中會產生對原有代碼的侵入式修改等問題。

 

另外,我也發現還是有人在都沒有理解清楚的前提下就做出了自己的解讀,流毒甚廣。我之前寫過關於CTMediator比較理論的描述,也有Demo,但惟獨沒有寫實踐方面的描述。我本來以為Demo就足夠了,可現在看來還是要給一篇實踐的文章的。

 

在更早之前,卓同學的swift老司機群里也有人提出因為自己並沒有理解透徹CTMediator方案,所以不敢貿然直接在項目中應用。所以這篇文章的另一個目的也是希望能夠讓大家明白,基於CTMediator的組件化方案實施其實非常簡單,而且也是有章法可循的。這篇文章可能會去討論一些理論的東西,但主要還會是以實踐為主。爭取做到能夠讓大家看完文章之后就可以直接在自己的項目中順利實施組件化。

 

最后,我希望這篇文章能夠終結業界持續近一年的關於組件化方案的無謂討論和錯誤討論。




准備工作



我在github上開了一個orgnization,里面有一個主工程:MainProject,我們要針對這個工程來做組件化。組件化實施完畢之后的主工程就是ModulizedMainProject了。抽出來的獨立Pod、私有Pod源也都會放在這個orgnization中去。

 

在一個項目實施組件化方案之前,我們需要做一個准備工作,建立自己的私有Pod源和快手工具腳本的配置:

 

  1. 先去開一個repo,這個repo就是我們私有Pod源倉庫
  2. pod repo add [私有Pod源倉庫名字] [私有Pod源的repo地址]
  3. 創立一個文件夾,例如Project。把我們的主工程文件夾放到Project下:~/Project/MainProject
  4. 在~/Project下clone快速配置私有源的腳本repo:git clone git@github.com:casatwy/ConfigPrivatePod.git
  5. 將ConfigPrivatePod的template文件夾下Podfile中source 'https://github.com/ModulizationDemo/PrivatePods.git'改成第一步里面你自己的私有Pod源倉庫的repo地址
  6. 將ConfigPrivatePod的template文件夾下upload.sh中PrivatePods改成第二步里面你自己的私有Pod源倉庫的名字



最后你的文件目錄結構應該是這樣:

 

Project
├── ConfigPrivatePod
└── MainProject



到此為止,准備工作就做好了。




實施組件化方案第一步:創建私有Pod工程和Category工程



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不存在任何依賴。

 

我們先創建A Pod




  1. 新建Xcode工程,命名為A,放到Projects下
  2. 新建Repo,命名也為A,新建好了之后網頁不要關掉

 

此時你的文件目錄結構應該是這樣:



Project
├── ConfigPrivatePod
├── MainProject
└── A



然后cd到ConfigPrivatePod下,執行./config.sh腳本來配置A這個私有Pod。腳本會問你要一些信息,Project Name就是A,要跟你的A工程的目錄名一致。HTTPS RepoSSH 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



同樣的,我們再創建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工程就准備好了。




實施組件化方案第二步:在主工程中引入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業務線對主工程的改動了。




實施組件化方案第三步:添加Target-Action,並讓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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM