iOS架構設計解耦的嘗試之VC邏輯AOP切割

一水發表於2019-02-27

該系列文章是2016年折騰的一個總結,對於這一年中思考和解決的一些問題做一些梳理和總結

上一篇文章iOS架構設計解耦的嘗試之模組間通訊中提到要說一下全域性UI堆疊是怎麼維護的。要寫的時候發現,這個東西背後還有一個更有意思的東西:使用AOP對VC的業務邏輯進行切割。在DZURLRoute中所使用到的全域性UI堆疊就是基於該思想構建出來的。這一部分的成果在庫DZViewControllerLifeCircleAction中總結成了Code(Talk is cheap. Show me the code)。而我們在
iOS架構設計系列之解耦的嘗試之變異的MVVM
中提到了通過MVVM來進行解耦,而這篇文章我們又通過另外一種方式AOP來嘗試進行解耦。感覺這一年在瘋狂的解耦:)。

AOP

先從AOP說起,其實在之前的文章中或者開發的庫中已經涉及到過很多次。比如對於Instance進行邏輯注入的庫MRLogicInjection,基於MRLogicInjection的應用方案用於相應區域擴充套件的DZExtendResponse、用於放重複點選的DZDeneyRepeat、用於介面上紅點提醒的MagicRemind。這一年對於AOP也算有了一個比較深入的實踐。而這次要說的VC邏輯切割,其實也算是AOP的一個實踐。說句題外話,Objective-C是門神奇的語言,他提供的動態性,讓我們可以對其進行很多有意思的改造,把OC改造成一個更好用的工具。而對其進行AOP改造就是我發現的非常有意思的一個事情。

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

上面這段文字摘自百度百科。對於AOP做了一個非常好的解釋,點選連結可以進去看看具體的內容。關於AOP只簡短的說一下我自己的理解,以作補充。

OC本來是個OOP的語言,我們通過封裝、繼承、多型來組織類之間的靜態結構,然後去實現我們的業務邏輯。只不過有些時候,嚴格遵循OOP的思想去設計繼承結構,會產生非常深的繼承關係。這勢必要增加整個系統的理解複雜度。而這並不是我們希望的。另外一點,我們講究設計的時候能夠滿足開閉原則,對變化是開放的,對於修改是封閉的。然而當我們的類繼承結構比較複雜的時候,就很難做到這一點。我們先來看一個比較Common的例子:

└── Object
    └── biont
        ├── Animal
        │   ├── cat
        │   └── dog
        └── plant複製程式碼

我們現在要構建一個用於描述生物的系統(精簡版),第一版我們做出了類似於上面的類結構。我們在Animal類中寫了cat和dog的公有行為,在cat和dog中各自描述了他們獨有的行為。這個時候突然發現我們多了一個sparrow物種。但是呢我們在Animal中描述的是動物都有四條腿,而sparrow只有兩條腿,於是原有的類結構就不能滿足現在的需求了,就得改啊。

└── Object
    └── biont
        ├── Animal
        │   ├── flying
        │   │   └── sparrow
        │   └── reptile
        │       ├── cat
        │       └── dog
        └── plant複製程式碼

為了能夠引入sparrow我們修改了Animal類,將四條腿的描述放到了reptile類中,並修改了Cat和Dog的繼承關係。修改的變動量還是不小的。引入了兩個新類,並對原有三個進行比較大的改動。

而如果用AOP的話我們會怎麼處理這個事情呢?切割和組合。

iOS架構設計解耦的嘗試之VC邏輯AOP切割

我們會將四條腿獨立出來,爬行切割出來,兩條腿切割出來,會飛切割出來
。。。然後dog就是四條腿爬行的動物。sparrow就是兩條腿會飛的動物。沒有了層次深的類繼承結構。更多的是組合,而一個具體的類更像是一個容器,用來容納不同的職責。當把這些不同的職責組合在一起的時候就得到了我們需要的類。AOP則提供一整套的瑞士軍刀,指導你如何進行切割,並如何進行組合。這也是我認為AOP的最大魅力。

DZViewControllerLifeCircleAction 對VC進行邏輯切割和組合

類似於上面我們提到的例子,我們在寫ViewController的業務邏輯的時候,也有可能造成非常深的繼承結構。而我們其實發現在眾多的業務邏輯中,有些東西是可以單獨抽離出來的。比如:

  1. 我們會在頁面第一次viewWillAppear的時候重新整理一次資料,這個在TableViewController會這樣,在CollectionViewController的時候也會這樣。
  2. 我們會在生命週期打Log,對使用者的使用路徑進行上報。
  3. ....

有些事情我們通過類整合來做了,比如打Log,找一個跟類,在裡面把打Log的邏輯寫了。但是當發現在繼承樹的末端有一個ViewController不需要打Log的時候就尷尬了。得大費周折的去改類結構,來適配這個需求。但是,如果這些業務邏輯像是積木一樣,需要的時候拿過來用,不需要的時候不管他,多好。這樣需要打Log的時候,拿過來一個打Log的積木堆進去,不需要的時候把打Log的積木拿走。

職責程式設計介面

而這就是AOP,面向切面程式設計。我們在ViewController上所選擇進行邏輯編制的切面就是UIViewController的各種展示回撥:

- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated複製程式碼

選擇這四個函式做為切面是因為在實際的程式設計過程中發現我們絕大多數的業務邏輯的起點都在這裡面,還有一些在viewDidLoad裡面。不過按照語義來講,viewDidLoad中應該是更多的對於VC中屬性變數的初始化工作,而不是業務邏輯的處理。在DZViewControllerLifeCircleAction的設計的時候,我們更多的是關注到ViewController的展示週期內會做的一些事情。就像:

  1. 一次展示的進行資料載入
  2. 展示的時候增加xxx的通知,在不展示的時候移除
  3. 在第一次展示的時候執行特殊的動作
  4. 構建特殊的頁面邏輯
  5. 。。。。。。

對應的我們在抽象出來的職責基類DZViewControllerLifeCircleBaseAction中提供了具體的程式設計介面:

/**
 When a instance of UIViewController's view will appear , it will call this method. And post the instance of UIViewController
 @param vc the instance of UIViewController that will appear
 @param animated  appearing is need an animation , this will be YES , otherwise NO.
 */
- (void) hostController:(UIViewController*)vc viewWillAppear:(BOOL)animated;
/**
 When a instance of UIViewController's view did appeared. It will call this method, and post the instance of UIViewController which you can modify it.
 @param vc the instance of UIViewController that did appeared
 @param animated appearing is need an animation , this will be YES, otherwise NO.
 */
- (void) hostController:(UIViewController*)vc viewDidAppear:(BOOL)animated;
/**
 When a instance of UIViewController will disappear, it will call this method, and post the instance of UIViewController which you can modify it.
 @param vc the instance of UIViewController that will disappear
 @param animated dispaaring is need an animation , this will be YES, otherwise NO.
 */
- (void) hostController:(UIViewController*)vc viewWillDisappear:(BOOL)animated;
/**
 When a UIViewController did disappear, it will call this method ,and post the instance of UIViewController which you can modify it.
 @param vc the instance of UIViewControll that did disppeared.
 @param animated disappearing is need an animation, this will be YES, otherwise NO.
 */
- (void) hostController:(UIViewController*)vc viewDidDisappear:(BOOL)animated;複製程式碼

一個獨立的職責可以整合基類建立一個子類,過載上述程式設計介面,進行邏輯編制。在展示週期內去寫自己都有的邏輯。這裡建議將這些邏輯儘可能的切割成粒度較小的邏輯單元。

在後續版本中也會考慮增加其他函式切入點的支援。

職責注入與刪除程式設計介面

而所有的這些職責,可以分成兩類:

  1. 通用職責,表現為所有的UIViewController都會有的職責,比如日誌Log。
  2. 專用職責,比如一個UITableViewController,需要在展示時才註冊xxx通知。

因而,在ViewController中設計職責容器的時候,也對應的設計了兩個職責容器:

DZViewControllerGlobalActions()用來承載通用職責

可以通過介面:


/**
 This function will remove the target instance from the global cache . Global action will be call when every UIViewController appear. if you want put some logic into every instance of UIViewController, you can user it.

 @param action the action that will be rmeove from global cache.
 */
FOUNDATION_EXTERN void DZVCRemoveGlobalAction(DZViewControllerLifeCircleBaseAction* action);



/**
 This function will add an instance of DZViewControllerLifeCircleBaseAction into the global cache. Global action will be call when every UIViewController appear. if you want put some logic into every instance of UIViewController, you can user it.

 @param action the action that will be insert into global cache
 */

FOUNDATION_EXTERN void DZVCRegisterGlobalAction(DZViewControllerLifeCircleBaseAction* action);複製程式碼

來增加或者刪除職責。

專用職責容器

可以通過下述介面進行新增或者刪除職責:

@interface UIViewController (appearSwizzedBlock)


/**
 add an instance of DZViewControllerLifeCircleBaseAction to the instance of UIViewController or it's subclass.
 @param action the action that will be inserted in to the cache of UIViewController's instance.
 */
- (DZViewControllerLifeCircleBaseAction* )registerLifeCircleAction:(DZViewControllerLifeCircleBaseAction *)action;


/**
 remove an instance of DZViewControllerLifeCircleBaseAction from the instance of UIViewController or it's subclass.
 @param action the action that will be removed from cache.
 */
- (void) removeLifeCircleAction:(DZViewControllerLifeCircleBaseAction *)action;
@end複製程式碼

使用舉例

LogAction

先拿我們剛才一直再說的Log的例子來說,我們可以寫一個專門打Log的Action:

@interface DZViewControllerLogLifeCircleAction : DZViewControllerLifeCircleBaseAction
@end


@implementation DZViewControllerLogLifeCircleAction

+ (void) load
{
    DZVCRegisterGlobalAction([DZViewControllerLogLifeCircleAction new]);
}
- (void) hostController:(UIViewController *)vc viewDidDisappear:(BOOL)animated
{
    [super hostController:vc viewDidDisappear:animated];
    [TalkingData trackPageBegin:YHTrackViewControllerPageName(vc)];

}
- (void) hostController:(UIViewController *)vc viewDidAppear:(BOOL)animated
{
    [super hostController:vc viewDidAppear:animated];
    [TalkingData trackPageEnd:YHTrackViewControllerPageName(vc)];
}
@end複製程式碼

在該類Load的時候將該Action註冊到通用職責容器中,這樣所有的ViewController都能夠打Log了。如果某一個ViewController不需要打Log可以直接選擇遮蔽掉該Action。

UIStack

好了,這個才是最終要說的正題。扯了半天,其實就是為了說這個全域性的展示的UIStack是怎麼維護的。首先要說明的是,此處的UIStack所維護的內容的是正在展示的ViewController的堆疊關係,而不是keywindow上ViewController的疊加關係。

當一個ViewController展示的時候他就入棧,當一個ViewController不在展示的時候就出棧。

因而在該UIStack中的內容是當前整個APP正在展示的ViewController的堆疊。而他的實現原理就是繼承DZViewControllerLifeCircleBaseAction並在viewAppear的時候入棧,在viewDisAppear的時候出棧。

@implementation DZUIStackLifeCircleAction

+ (void) load
{
    DZUIShareStack = [DZUIStackLifeCircleAction new];
    DZVCRegisterGlobalAction(DZUIShareStack);
}

- (void) hostController:(UIViewController *)vc viewDidAppear:(BOOL)animated
{
    [super hostController:vc viewDidAppear:animated];
    //入棧
    if (vc) {
        [_uiStack addPointer:(void*)vc];
    }
}

//出棧
- (void) hostController:(UIViewController *)vc viewDidDisappear:(BOOL)animated
{
    [super hostController:vc viewDidDisappear:animated];
    NSArray* allObjects = [_uiStack allObjects];
    for (int i = (int)allObjects.count-1; i >= 0; i--) {
        id object = allObjects[i];
        if (vc == object) {
            [_uiStack replacePointerAtIndex:i withPointer:NULL];
        }
    }
    [_uiStack compact];
}
....
@end複製程式碼

同樣也註冊為一個通用職責。上面這兩個例子下來,就已經在ViewController中加入了兩個通用職責了。而這些職責之間都是隔離的,是程式碼隔離的那種!!!

執行一次的Action, 專用職責的例子

在ViewController程式設計的時候,我們經常會寫一些類似於_firstAppear這樣的BOOL型別的變數,來標記這個VC是第一次被展示,然後做一些特定的動作。其實這個就是在VC所有的展示週期內只做一次的操作,真對這個需求我們可以寫一個這樣的Action:


/**
 The action block to handle ViewController appearing firstly.

 @param vc The UIViewController tha appear
 @param animated It will aminated paramter from the origin SEL paramter.
 */
typedef void (^DZViewControllerOnceActionWhenAppear)(UIViewController* vc, BOOL animated);

/**
 when a ViewController appear firstly , it will do something . This class is design for this situation
 */
@interface DZVCOnceLifeCircleAction : DZViewControllerLifeCircleBaseAction


/**
 The action block to handle ViewController appearing firstly.
 */
@property (nonatomic, strong) DZViewControllerOnceActionWhenAppear actionBlock;


/**
 Factory method to reduce an instance of DZViewControllerOnceActionWhenAppear
 @param block The handler to cover UIViewController appearing firstly
 @return an instance of DZViewControllerOnceActionWhenAppear
 */
+ (instancetype) actionWithOnceBlock:(DZViewControllerOnceActionWhenAppear)block;



/**
 a once action is an class that handle some logic once when one instance of UIViewController appear. It need a block to exe the function.

 @param  the logic function to exe
 @return an instance of DZVCOnceLifeCircleAction
 */
- (instancetype) initWithBlock:(DZViewControllerOnceActionWhenAppear)block;

@end複製程式碼

該Action預設包含在DZViewControllerLifeAction庫中了。當有VC需要這種指責的時候直接注入就行了,例如:

  [tableVC registerLifeCircleAction:[DZVCOnceLifeCircleAction actionWithOnceBlock:^(UIViewController *vc, BOOL animated) {
                [[DZContactMonitor userMonitor] asyncLoadSystemContacts];
            }]];複製程式碼

其他

上面我們舉了通用職責和專用職責的例子,都還算是比較簡單的例子。其實,就是希望把職責拆解成粒度更小的單元。然後組合使用。而在我的APP中還有更加複雜的關於應用ViewController的AOP的例子。我把一個整個邏輯模組,比如彈幕功能做為了一個邏輯單元,基於DZViewControllerLifeAction來寫,當某個介面需要彈幕的時候,就當做專用職責進行邏輯注入。而這樣一來,發現你完全可以複用一整塊原先可能完全不能複用的邏輯。在解耦和複用這條路上,這種方式算是目前我做的比較瘋狂的事情了。非常有意思。

歡迎關注iOS開發公共賬號 iOS開發知識 :掃描下方二維碼關注

iOS架構設計解耦的嘗試之VC邏輯AOP切割

版權宣告:轉載請保留作者資訊,惡意轉載作者將保留追究法律責任的權利。BY:yishuiliunian

相關文章