讲一个例子谈谈对组件化和模块化编程的一些思考

一个通用模块 BFRouter 的诞生

我们的 app 存在多个地方的唤起, 主要包括

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

由于不是一个人的开发或版本的不同,我们的代码是这样的:
有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页的跳转统一管理,灵活配置。

最终你渴望的样子:

- (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

组件定义:可独立发布的二进制单元,单独开发,编译和单独测试

  1. 黑盒,具有版本号,配置并调用接口使用。
  2. 接口-实现分离,组件间通过接口联系
  3. 自行管理内部的一个或多个类(模块化管理)

软件设计的发展:

  1. 功能分解法--计算任务
  2. 结构化程序设计--以数据为中心
  3. 面向对象的程序设计--已对象为中心
  4. 组件化程序设计-- 以组件为中心

难道模块化跟组件化真的是完全一样的?的确,很多时候两者的概念完全可以相互替换,在实践中更是经常混用。
模块化强调的是拆分,无论是从业务角度还是从架构、技术角度,模块化首先意味着将代码、数据等内容按照其职责不同分离,使其变得更加容易维护、迭代,使开发人员可以分而治之。
组件化则着重于可重用性,不管是界面上反复使用的用户头像按钮,还是处理数据的流程中的某个部件,只要可以被反复使用,并且进行了高度封装,只能通过接口访问,就可以称其为“组件”。

总结:

  • 模块是站在一个完整的应用程序中来讲的, 更多是站在业务的角度。一个通用模块的抽离封装,就是一个组件。
  • 组件是站在不同的应用程序程序来讲的。可脱离当前应用程序,可替换,可移植。是对一项独立完整功能的模块化封装。
  • ios 中的 dylibs, sdk, open library, .a 都可以称为组件。

2. 为什么?优点,缺点

一派是说app开发并不需要什么狗P架构,第二派说我们有自己NB的架构,第三派说只要模块化够好,每个模块应该有自己的架构。

这三个观点的出发点,我觉得也比较好理解,第一种应该是一些个人开发者,个人能力很强,经常一个人很快搞出来一个app,他的映像中不需要弄太多的框框框住自己,但是其实他也是有一套自己的架构的。第二派应该是一些公司或者大公司,有一套NB的架构对于团队的意义就比较大了,可以保证稳定迭代,保证规范和持久可维护性。第三派应该是BAT这样的有很多BU的超级公司,或者一些先进的开源开发者们,模块化能够更好的实现跨app的代码和功能的复用, 能够更好的共享资源,避免重复造轮子。

优点

1、不只提高了代码的复用度,还可以实现真正的功能复用,比如同样的功能模块如果实现了自完备性,可以在多个app中复用
2、业务隔离,跨团队开发代码控制和版本风险控制的实现
3、模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力。

缺点, 模块化当然也有它的缺点:

1、入门门槛较高,新手入门需要的成本也更高
2、工具的使用成本,团队间和模块间的配合成本升高,开发效率短期会降低。
但是从长期的影响来说,带来的好处远大于坏处的,因此模块化仍然是最佳的架构选择。

3. 模块化的方法、基本原则

无论是模块化还是组件化,首先肯定是做拆分,但是如何拆分?怎么下手?依照什么标准?

3.1 一些简单方法。

业务层面:

很多时候,一个完整的软件程序是同时为多种业务服务的,所有可以优先按照业务的不同,将整个系统进行拆分。

如一个电商类型的App,就可以分出商品浏览模块、订单模块、购物车模块、消息模块、支付模块等。又如微信这种社交型应用,可以拆分出联系人模块、朋友圈模块、聊天模块、消息模块等。


其实就是从用户使用的角度,按照功能的不同划分模块,当然,这种业务模块是要由各种技术模块作支撑的。

架构层面:

如果脱离业务,只从技术角度来看,则可以尝试纵向对系统拆分模块。

其实这里的纵向拆分跟对系统的架构做分层有点像=。=,现如今只要需要联网请求API的App都免不了有网络请求、数据缓存、数据加工处理、数据展示、反馈用户操作等行为,所有这些环节层层递进才能完成一个功能。

当开始着手规划一个完整软件系统,或者说App时,就可以按照这些环节划分模块,纵向分层次的组合,搭建出一个以技术模块组成的简易系统架构图,方便后续的开发,如下图



大体上的技术模块划分好以后,就可以按照具体的需求,实现每个技术模块,乃至细分出更多的子模块,如缓存模块可能由键值对缓存(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模块化编程的世界中,有两个规范不得不提,它们分别是AMD和CMD。现在的JS库或框架,凡是模块化的,一般都是遵循了这两个规范其中之一。
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应该属于一个模块,要么就是整体的拆分有问题。一个完整的软件系统的模块依赖应该是一张有向无环图。
  • 紧凑性,模块、组件对外暴露的接口、属性应该尽可能的少,接口的参数个数也要少。
  • 面向接口,模块、组件对外提供服务时最好是面向接口的,以便后期可以灵活的变更实现。

总结:

  1. 模块最重要的属性是它们应该尽可能的独立和自包含;模块应被设计成可以提供一整套功能,以便程序的其它部分与它清楚地相互作用;模块提供的功能必须是完整的,以便它的调用者们可以各取所需。

  2. 模块化就是为了减少循环依赖,减少耦合,提高设计和开发的效率。为了做到这一点,我们需要有一个设计规则,所有的模块都在这个规则下进行设计。良好的设计规则,会把耦合密集的设计参数进行归类作为一个模块,并以此划分工作任务。而模块之间彼此通过一个固定的接口进行交互,除此之外 的内部实现则由模块的开发团队进行自由发挥。

  3. 最后但也是重要的一点:方法命名的规范性很重要,注释很重要,如果没有注释只有开发者心中很清楚,所以必要的注释会给后期的代码维护工作带来便利的同时也提高效率。每个界面的主要是用于做什么的,可以在头文件中适当进行说明。

参考文献:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容