講一個例子談談對組件化和模塊化編程的一些思考

一個通用模塊 BFRouter 的誕生

我們的 app 存在多個地方的喚起, 主要包括

  1. push 的喚起
  2. 其他 app通過 scheme
  3. ios 9通過apple-app-site-association 。

由于不是一個人的開發(fā)或版本的不同,我們的代碼是這樣的:
有scheme 這樣的:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    if (url) {
        // bdlicai://baidu/home
        // bdlicai://activitydetail?url=xxx
        // bdlicai://activitylist
        // bdlicai://messagedetail?id=xxx&title=xxx
        
        NSString *host = url.host;
        NSDictionary *paramDict = [url.query parseUrlParamToDict];
        if ([host isEqualToString:@"home"]) {
            for (BJNavigationController *nav in self.tabBarVC.viewControllers) {
                [nav popToRootViewControllerAnimated:NO];
            }
            [self.tabBarVC setSelectedIndex:0];
        } else if ([host isEqualToString:@"activitydetail"]) {
            NSString *url = [paramDict valueForKey:@"url"];
            NSString *codeUrl = [url
                                 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
            [self.rootNav openWebContainerWithUrl:codeUrl title:@"活動詳情" handle:nil];
        } else if ([host isEqualToString:@"activitylist"]) {
            [self.rootNav openWebContainerWithUrl:kActivityListUrl title:@"活動" handle:nil];
        } else if ([host isEqualToString:@"messagedetail"]) {
            NSString *title = [paramDict valueForKey:@"title"];
            NSString *msgId = [paramDict valueForKey:@"id"];
            [self.rootNav openWebContainerWithUrl:MessageDetailUrl(msgId) title:title handle:nil];
        } else {
            return [[BFShareController sharedInstance] handleShareOpenURL:url];
        }
            
        
        return YES;
    }
    
    return NO;
}

還有apple-app-site-association 是這樣的:

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler
{
    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        NSURL *webpageURL = userActivity.webpageURL;
        NSString *host = webpageURL.host;
        if ([host isEqualToString:@"8.baidu.com"]) {
            NSString *urlPath = webpageURL.path;
            if ([urlPath isEqualToString:@"/link/webview"]) {
                NSString *urlQuery = webpageURL.query;
                if (STRINGHASVALUE(urlQuery)) {
                    NSRange keyRane = [urlQuery rangeOfString:@"url="];
                    if (keyRane.length !=0) {
                        NSString *url = [urlQuery substringFromIndex:keyRane.length];
                        if (STRINGHASVALUE(url)) {
                           NSString *encodeUrlString =  [url stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
                            [self.rootNav openWebContainerWithUrl:encodeUrlString title:nil handle:nil];
                        }
                    }
                }

            }else if ([urlPath isEqualToString:@"/link/native/home"]) {
                for (BJNavigationController *nav in self.tabBarVC.viewControllers) {
                    [nav popToRootViewControllerAnimated:NO];
                }
                [self.tabBarVC setSelectedIndex:0];
            }else if ([urlPath isEqualToString:@"/link/native/finance"]) {
                BJNavigationController *nav = [self.tabBarVC.viewControllers objectAtIndex:1];
                [nav popToRootViewControllerAnimated:NO];
                [self.tabBarVC setSelectedIndex:1];
                BFInvestHomeViewController *investHomeVC = (BFInvestHomeViewController*)[nav.viewControllers firstObject];
                investHomeVC.selectedIndex = 0;
            }else if ([urlPath isEqualToString:@"/link/native/fund"]) {
                BJNavigationController *nav = [self.tabBarVC.viewControllers objectAtIndex:1];
                [nav popToRootViewControllerAnimated:NO];
                [self.tabBarVC setSelectedIndex:1];
                BFInvestHomeViewController *investHomeVC = (BFInvestHomeViewController*)[nav.viewControllers firstObject];
                investHomeVC.selectedIndex = 1;
            }
        }
    }
    return YES;
    
}
#endif

還有 push 這樣的:

- (void)pushJumpWithPushInfo:(NSDictionary *)pushInfo animation:(BOOL)animation isLaunching:(BOOL)isLaunching {
    if (![pushInfo.allKeys containsObject:@"assetType"]) {
        return;
    }
    
    // to 資產列表(1: 定期 2: 混合債券指數)type = 3
    NSString *assetType = [pushInfo valueForKey:@"assetType"];
    if ([assetType isEqualToString:@"1"]) {
        [self.appdelegate.rootNav openWebContainerWithUrl:FinanceRegularListUrl title:@"定期理財" handle:nil];
    }else if ([assetType isEqualToString:@"2"]) {
        for (BJNavigationController *nav in self.appdelegate.tabBarVC.viewControllers) {
            [nav popToRootViewControllerAnimated:NO];
        }
        [self.appdelegate.tabBarVC setSelectedIndex:1];
        BJNavigationController *nav = (BJNavigationController *)self.appdelegate.tabBarVC.selectedViewController;
        BFInvestHomeViewController *investHomeVC = (BFInvestHomeViewController*)[nav.viewControllers firstObject];
        @try {
            investHomeVC.selectedIndex = 1;
        }
        @catch (NSException *exception) {
        }
    }
}

看到如此雷同的功能,作為程序員的我們真心不能忍,不能忍?。?!

這樣一個模塊的原始需求就產生了~
原始需求+擴展需求+封裝+接口 = 公用模塊

借鑒蘑菇街MGJRouter一個思路, 實現咱們自己輕量級的Light-BFRouter, 一個高效靈活的router, 對native頁和h5頁的跳轉統(tǒng)一管理,靈活配置。

最終你渴望的樣子:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    if (url) {
        // scheme 增加host:finance
        
        // bdlicai://finance/home
        // bdlicai://finance/activitydetail?url=xxx
        // bdlicai://finance/activitylist
        // bdlicai://finance/messagedetail?id=xxx&title=xxx
        
        NSString *host = url.scheme;
        if ([host isEqualToString:kScheme_bdlicai]) {
           return [[BFRouter routerForScheme:kScheme_bdlicai] routeURL:url];
        } else {
            return [[BFShareController sharedInstance] handleShareOpenURL:url];
        }
    }
    
    return NO;
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler {
    
    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
         NSURL *webpageURL = userActivity.webpageURL;
        [[BFRouter routerForScheme:kScheme_https] routeURL:webpageURL];
    }
    return YES;
}
#endif
- (void)pushJumpWithPushInfo:(NSDictionary *)pushInfo animation:(BOOL)animation isLaunching:(BOOL)isLaunching {
    if (![pushInfo.allKeys containsObject:@"assetType"]) {
        return;
    }
    
    // to 資產列表(1: 定期 2: 混合債券指數)type = 3
    NSString *assetType = [pushInfo valueForKey:@"assetType"];
    if ([assetType isEqualToString:@"1"]) {
        // 定期產品列表
        NSString *url = [NSString stringWithFormat:@"%@?url=%@", kRoutePattern_Common_WebView, FinanceRegularListUrl];
        [[BFRouter routerForScheme:kScheme_bdlicai] routeURL:[NSURL URLWithString:url]];
    }else if ([assetType isEqualToString:@"2"]) {
        NSURL *url = [NSURL URLWithString:kRoutePattern_Common_Native_Fund];
        [[BFRouter routerForScheme:kScheme_bdlicai] routeURL:url];
    }
}

清爽如你,這下心靜了??!


1. 概念

模塊化編程

Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality.
With modular programming, concerns are separated such that modules perform logically discrete functions, interacting through well-defined interfaces.
https://en.wikipedia.org/wiki/Modular_programming#Key_aspects

看個圖:


組件化編程

Component-based software engineering (CBSE), also known as component-based development (CBD), is a branch of software engineering that emphasizes the separation of concerns in respect of the wide-ranging functionality available throughout a given software system. It is a reuse-based approach to defining, implementing and composing loosely coupled independent components into systems.
https://en.wikipedia.org/wiki/Component-based_software_engineering

組件定義:可獨立發(fā)布的二進制單元,單獨開發(fā),編譯和單獨測試

  1. 黑盒,具有版本號,配置并調用接口使用。
  2. 接口-實現分離,組件間通過接口聯(lián)系
  3. 自行管理內部的一個或多個類(模塊化管理)

軟件設計的發(fā)展:

  1. 功能分解法--計算任務
  2. 結構化程序設計--以數據為中心
  3. 面向對象的程序設計--已對象為中心
  4. 組件化程序設計-- 以組件為中心

難道模塊化跟組件化真的是完全一樣的?的確,很多時候兩者的概念完全可以相互替換,在實踐中更是經常混用。
模塊化強調的是拆分,無論是從業(yè)務角度還是從架構、技術角度,模塊化首先意味著將代碼、數據等內容按照其職責不同分離,使其變得更加容易維護、迭代,使開發(fā)人員可以分而治之。
組件化則著重于可重用性,不管是界面上反復使用的用戶頭像按鈕,還是處理數據的流程中的某個部件,只要可以被反復使用,并且進行了高度封裝,只能通過接口訪問,就可以稱其為“組件”。

總結:

  • 模塊是站在一個完整的應用程序中來講的, 更多是站在業(yè)務的角度。一個通用模塊的抽離封裝,就是一個組件。
  • 組件是站在不同的應用程序程序來講的??擅撾x當前應用程序,可替換,可移植。是對一項獨立完整功能的模塊化封裝。
  • ios 中的 dylibs, sdk, open library, .a 都可以稱為組件。

2. 為什么?優(yōu)點,缺點

一派是說app開發(fā)并不需要什么狗P架構,第二派說我們有自己NB的架構,第三派說只要模塊化夠好,每個模塊應該有自己的架構。

這三個觀點的出發(fā)點,我覺得也比較好理解,第一種應該是一些個人開發(fā)者,個人能力很強,經常一個人很快搞出來一個app,他的映像中不需要弄太多的框框框住自己,但是其實他也是有一套自己的架構的。第二派應該是一些公司或者大公司,有一套NB的架構對于團隊的意義就比較大了,可以保證穩(wěn)定迭代,保證規(guī)范和持久可維護性。第三派應該是BAT這樣的有很多BU的超級公司,或者一些先進的開源開發(fā)者們,模塊化能夠更好的實現跨app的代碼和功能的復用, 能夠更好的共享資源,避免重復造輪子。

優(yōu)點

1、不只提高了代碼的復用度,還可以實現真正的功能復用,比如同樣的功能模塊如果實現了自完備性,可以在多個app中復用
2、業(yè)務隔離,跨團隊開發(fā)代碼控制和版本風險控制的實現
3、模塊化對代碼的封裝性、合理性都有一定的要求,提升開發(fā)同學的設計能力。

缺點, 模塊化當然也有它的缺點:

1、入門門檻較高,新手入門需要的成本也更高
2、工具的使用成本,團隊間和模塊間的配合成本升高,開發(fā)效率短期會降低。
但是從長期的影響來說,帶來的好處遠大于壞處的,因此模塊化仍然是最佳的架構選擇。

3. 模塊化的方法、基本原則

無論是模塊化還是組件化,首先肯定是做拆分,但是如何拆分?怎么下手?依照什么標準?

3.1 一些簡單方法。

業(yè)務層面:

很多時候,一個完整的軟件程序是同時為多種業(yè)務服務的,所有可以優(yōu)先按照業(yè)務的不同,將整個系統(tǒng)進行拆分。

如一個電商類型的App,就可以分出商品瀏覽模塊、訂單模塊、購物車模塊、消息模塊、支付模塊等。又如微信這種社交型應用,可以拆分出聯(lián)系人模塊、朋友圈模塊、聊天模塊、消息模塊等。


其實就是從用戶使用的角度,按照功能的不同劃分模塊,當然,這種業(yè)務模塊是要由各種技術模塊作支撐的。

架構層面:

如果脫離業(yè)務,只從技術角度來看,則可以嘗試縱向對系統(tǒng)拆分模塊。

其實這里的縱向拆分跟對系統(tǒng)的架構做分層有點像=。=,現如今只要需要聯(lián)網請求API的App都免不了有網絡請求、數據緩存、數據加工處理、數據展示、反饋用戶操作等行為,所有這些環(huán)節(jié)層層遞進才能完成一個功能。

當開始著手規(guī)劃一個完整軟件系統(tǒng),或者說App時,就可以按照這些環(huán)節(jié)劃分模塊,縱向分層次的組合,搭建出一個以技術模塊組成的簡易系統(tǒng)架構圖,方便后續(xù)的開發(fā),如下圖



大體上的技術模塊劃分好以后,就可以按照具體的需求,實現每個技術模塊,乃至細分出更多的子模塊,如緩存模塊可能由鍵值對緩存(NSUserDefaults)、數據庫緩存(SQLite、Realm)、圖片緩存等子模塊組成,根據具體情況而定。

功能層面:

  1. 從界面入手,拆分可視化組件

功能層面的模塊劃分,是為了功能獨立,實現高內聚,低耦合。
每一個小的功能模塊能運行,能調試,能測試,各個功能之間基本是完全獨立的,不存在相互依賴的關系。
現在再來看看如何從界面入手拆分可復用的組件。假如有如下布局的界面:

很多時候,像界面里面的“搜索框”、“頭像按鈕”、“內容框”和顯示提示用的“加載中”HUD,甚至整個內容的Cell,都是可能在很多地方出現的,而且本身的樣式、功能比較集中。
如頭像可能要支持點擊跳轉,頭像圖片圓角,內容框有特定的Padding和字體大小等,所以可以將這些界面上的元素“提”出來,單獨封裝成一個組件,供整個App復用。或者直接用第三方的組件,如圖中的“加載中”HUD,就可以用SVProgressHUD、MBProgressHUD等開源庫。

2.從數據入手,拆分數據加工組件

再來看看從數據入手,拆分可復用的組件。假如有如下數據處理流程:



其實大部分時候,拆分模塊、組件都是以清晰的流程、邏輯為基礎的,就如上圖的過程,當流程清晰后,可以拆分復用的組件也就“出來了”。

如從JSON數據實例化出對應的Entity對象,這個功能就是一個完整獨立的組件.

組件本身負責自己的所有功能、樣式。

3.2 沒有模塊化的 js 是怎么做的

js 的模塊化
https://segmentfault.com/a/1190000000492678
AMD 與 CMD
在JavaScript模塊化編程的世界中,有兩個規(guī)范不得不提,它們分別是AMD和CMD。現在的JS庫或框架,凡是模塊化的,一般都是遵循了這兩個規(guī)范其中之一。
CommonJS
http://wiki.commonjs.org/wiki/CommonJS
Sea.js
http://seajs.org/docs/#docs
https://github.com/seajs/seajs/issues/242

3.3 幾個原則

  • 單一職責,意味著一個模塊、一個組件只做一件事,絕不多做。
  • 正交性,意思是不重復,一個模塊跟另一個模塊的職責是正交的,沒有重疊,組件也是一樣。
  • 單向依賴,模塊之間最多是單向的依賴,如果出現A依賴B,B也依賴A,那么要么是A、B應該屬于一個模塊,要么就是整體的拆分有問題。一個完整的軟件系統(tǒng)的模塊依賴應該是一張有向無環(huán)圖。
  • 緊湊性,模塊、組件對外暴露的接口、屬性應該盡可能的少,接口的參數個數也要少。
  • 面向接口,模塊、組件對外提供服務時最好是面向接口的,以便后期可以靈活的變更實現。

總結:

  1. 模塊最重要的屬性是它們應該盡可能的獨立和自包含;模塊應被設計成可以提供一整套功能,以便程序的其它部分與它清楚地相互作用;模塊提供的功能必須是完整的,以便它的調用者們可以各取所需。

  2. 模塊化就是為了減少循環(huán)依賴,減少耦合,提高設計和開發(fā)的效率。為了做到這一點,我們需要有一個設計規(guī)則,所有的模塊都在這個規(guī)則下進行設計。良好的設計規(guī)則,會把耦合密集的設計參數進行歸類作為一個模塊,并以此劃分工作任務。而模塊之間彼此通過一個固定的接口進行交互,除此之外 的內部實現則由模塊的開發(fā)團隊進行自由發(fā)揮。

  3. 最后但也是重要的一點:方法命名的規(guī)范性很重要,注釋很重要,如果沒有注釋只有開發(fā)者心中很清楚,所以必要的注釋會給后期的代碼維護工作帶來便利的同時也提高效率。每個界面的主要是用于做什么的,可以在頭文件中適當進行說明。

參考文獻:

http://casatwy.com/iOS-Modulization.html
https://blog.cnbluebox.com/blog/2015/11/28/module-and-decoupling/
http://tutuge.me/2016/03/29/modular-and-component-summary/
http://www.tqcto.com/article/mobile/102970.html
http://www.cocoachina.com/ios/20160929/17610.html

蘑菇街的組件化-MGJRouter
https://github.com/mogujie/MGJRouter

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容