模块化开发浅析

背景:由于目前公司的iOS项目的依赖管理是比较原始的状态,但是APP功能又是越来越复杂的,这就带来的很多问题,比如开发时编译时间过长、模块间耦合严重、模块依赖混乱等。最近又听说这个项目中的部分功能可能需要独立出一个新APP,本着“Don't repeat yourself”的原则,试着抽离出原项目中的各个模块,并在新的APP中集成这些模块。
最近算是初步完成了新APP的模块化,也算是从中总结了一些经验拿出来分享一下。同时也完成了一个模块化框架TinyPart欢迎star。

模块划分
做模块化还是要结合实际业务,对目前APP的功能做一个模块划分,在划分模块的时候还需要关注模块之间的层级。

比如说,在我们项目中,模块被分成了3个层级:基础层、中间层、业务层。基础层模块比如像网络框架、持久化、Log、社交化分享这样的模块,这一层的模块我们可以称之为组件,具有很强的可重用性。中间层模块可以有登录模块、网络层、资源模块等,这一层模块有一个特点是它们依赖着基础组件但又没有很强的业务属性,同时业务层对这层模块的依赖是很强的。业务层模块,就是直接和产品需求对应的模块了,比如类似朋友圈、直播、Feeds流这样的业务功能了。

代码隔离
模块化首先要做的是代码层面上独立,任意一个基础模块都是可以独立编译的,底层模块绝对不能有对上层模块的代码依赖,还要确保未来也不会再出现这样的代码。

在这里我们选择使用CocoaPods来确保模块间代码隔离,基础和中间层模块是一定会做成标准的私有pods组件,加入到私有pods仓库。业务层的模块,则不一定非要加到私有pods仓库中,也可以使用submodule + local pods的方案。这样做有两个原因:其一是业务模块的改动往往比较频繁,如果是标准的私有pods组件则需要频繁的操作pod install或者pod update;其二是如果是local pod会直接引用对应仓库的源文件,在主工程对pods工程下业务模块的改动就是直接对其git仓库的改动,没有了频繁的pod repo push和pod install操作。

依赖管理
选择使用CocoaPods另外一个重要原因就是,可以通过它来管理模块间的依赖,之前项目各个功能之所以难以复用的重要原因之一就是没有声明依赖。这里的依赖不仅仅是A模块依赖B模块这样的事情,还可以是A模块运行需要的所有工程配置,比如A模块工程需要添加一个GCC_PREPROCESSOR_DEFINITIONS预处理宏才能正常编译。因此,个人认为模块依赖声明非常重要,即便没有像CocoaPods这样的管理工具,也应该有相关文档来说明每个内部模块或者SDK的依赖。

CocoaPods的方便之处就在于你必须把你模块的依赖列出来,否则是无法通过pod spec lint过程的,并且所有的依赖项也都是必须是pods仓库。除此以外,依赖的集成也是自动化的,CocoaPods可以自动地添加工程配置和依赖组件。

模块集成
在完成上述两个步骤以后,模块化工程的构建工作基本就结束了,接下来我们探讨一下如何在工程中更好地使用这些模块。为此我们写了一个组件化的开源方案 TinyPart。

一般来说,模块初始化需要在APP启动或者UI初始化附近的时机来完成,有时候各个模块的启动顺序可能也是有讲究的,这些初始化逻辑我们往往会加入到AppDelegate这个类里。过一段时间我们会发现,AppDelegate这个类变得臃肿不堪,逻辑复杂,难以维护。在TinyPart中,Module的声明协议包含了UIApplicationDelegate,这就意味着每一个模块都可以实现有一套自己的UIApplicationDelegate协议,并且它们之间调用顺序是可以自定义的。

@interface TPLShareModule : NSObject <TPModuleProtocol>
@end
@implementation TPLShareModule
TP_MODULE_ASYNC
 
TP_MODULE_PRIORITY(TPLSHARE_MODULE_PRIORITY)
 
- (void)moduleDidLoad:(TPContext *)context {
  [WXApi registerApp:APPID];
}
 
- (BOOL)application:(UIApplication *)application
      openURL:(NSURL *)url
 sourceApplication:(NSString *)sourceApplication
     annotation:(id)annotation {
  return [WXApi handleOpenURL:url delegate:self];
}
 
- (BOOL)application:(UIApplication *)app
      openURL:(NSURL *)url
      options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  return [WXApi handleOpenURL:url delegate:self];
}
@end

上面的代码是一个微信社交分享模块的初始化内容,同时实现了微信分享所要求的UIApplicationDelegate中的方法。

通信
消息
在面向对象中,消息是一个十分重要的概念,它是对象之前通信的重要方式。但是,在OC中如果想要向一个对象发消息,正常做法就是将改对象类的头文件import进来,这样我们就能够写出[aInstance method]这样的代码了。

然而在模块化中,我们并不希望模块与模块之间相互引用各自的类文件,但是又想要实现通信,那怎么办呢?通过协议来完成。我们知道OC是一个动态语言,方法的调用过程其实是动态的,头文件中消息方法的声明只是为了通过编译前的静态检查。也就是说,我们只要写一个协议来告诉编译器有这么一个方法就可以了,至于实际上究竟有没有这个方法是在消息发过去以后就知道了。既然OC有这个特性,我们甚至可以直接通过类名和方法名向一个对象发送消息,这其实就是网上大部分组件化路由的实现机制。

因此在TinyPart中我们既提供了协议和路由两种模式来调用模块内的服务。

@protocol TestModuleService1 <TPServiceProtocol>
- (void)function1;
@end
 
@interface TestModuleService1Imp : NSObject <TestModuleService1>
@end
 
@implementation TestModuleService1Imp
TPSERVICE_AUTO_REGISTER(TestModuleService1) // Service will be registered in "+load" method
 
- (void)function1 {
  NSLog(@"%@", @"TestModuleService1 function1");
}
@end

上面的代码中,我们定义了一个服务的协议。

#import "TestModuleService1.h"
 
id<TestModuleService1> service1 = [[TPServiceManager sharedInstance] serviceWithName:@"TestModuleService1"];
 
[service1 function1];

这里我们只需要import上述协议的头文件,然后就可以向TestModuleService1发消息了。

我们看到上述的跨模块调用方案中,只需要暴露一个协议文件就可以了,下面我们再看一下如何用路由的方式来做到完全不暴露任何头文件。

#import "TPRouter.h"
 
@interface TestRouter : TPRouter
@end
 
@implementation TestRouter
TPROUTER_METHOD_EXPORT(action1, {
  NSLog(@"TestRouter action1 params=%@", params);
  return nil;
});
 
TPROUTER_METHOD_EXPORT(action2, {
  NSLog(@"TestRouter action2 params=%@", params);
  return nil;
});
@end

在这里我们参考了ReactNative的方案,通过一个TPROUTER_METHOD_EXPORT宏来定义一个可供调用的路由服务,同时可以传一个params参数进了。然后我们再来调用这个路由。

[[TPMediator sharedInstance] performAction:@"action1" router:@"Test" params:@{}];

通知

除了上面提到的两种普通的模块通信方案,我们发现在项目中经常会有跨模块的NSNotification,对于这样的观察者模式使用NSNotification来实现是最便捷的方式了。尽管NSNotification可以做到模块间解耦,但是对于通知的管理过于松散会导致散落在各个模块的NSNotification逻辑变得十分复杂,因此我们为TinyPart增加了一种有向通信的方案。

所谓有向通信,则是在NSNotification基础上对通知的传播方向进行了限制,底层模块对上层模块的通知称为广播Broadcast,上层模块对底层模块或者同层模块的通知称为上报Report。这样做有两个好处:一方面更利于通知的维护,另一方面可以帮助我们划分模块层级,如果我们发现有一个模块需要向多个同级模块进行Report那么这个模块很有可能应该被划分到更底层的模块。

用法同NSNotification类似,只不过创建通知的方法是一个链式调用,大概就是这样:

// 发送
TPNotificationCenter *center2 = [TestModule2 tp_notificationCenter];
 
[center2 reportNotification:^(TPNotificationMaker *make) {
  make.name(@"report_notification_from_TestModule2");
} targetModule:@"TestModule1"];
   
[center2 broadcastNotification:^(TPNotificationMaker *make) {
  make.name(@"broadcast_notification_from_TestModule2").userInfo(@{@"key":@"value"}).object(self);
}];
 
// 接收
TPNotificationCenter *center1 = [TestModule1 tp_notificationCenter];
[center1 addObserver:self selector:@selector(testNotification:) name:@"report_notification_from_TestModule2" object:nil];

文章来源于网络,如有侵权,请联系小编删除。

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

推荐阅读更多精彩内容