一款令人愉悦的应用业务模块化架构库。
什么是 Bifrost(/ˈbɪvrɒst/)
Bifrost 是一款为应用业务模块化架构而设计的令人愉悦的库。其名称源自北欧神话以及著名漫威电影《雷神》。Bifrost 是一座彩虹桥,人们可以通过它瞬间到达任何地方。
一些术语
首先,让我们同步一些我们将要使用的术语。
业务模块 vs 功能模块
通常,我们将用于非业务功能的库称为功能模块,如AFN、SDWebImage、Masonry等。它们可以跨各种应用程序使用,并提供许多API。应用程序需要导入它们的API声明并直接调用它们的API。而业务模块包含大量的业务代码,如商品模块、贸易模块等。它们只能在有限数量的应用程序中使用(有时仅1个),并且仅出口少量的API或路由URL,因为业务模块不能从其他业务模块导入任何API声明。
代码依赖性 vs 业务依赖性
代码依赖性指的是对代码的依赖。如果A模块依赖于B模块的代码,A模块导入了B模块的一些API声明。换句话说,没有B模块的代码,A模块无法成功构建。业务依赖性指的是对其他模块的业务需求。例如,如果贸易模块需要显示商品详情页面,它就依赖于商品模块。业务依赖性可以通过业务模块化架构来消除,但业务依赖性始终存在。
业务模块化架构(BMA)
随着应用程序变得越来越复杂,不同业务模块之间会有很多依赖。一个文件的更改会导致许多文件受到影响。开发效率受到影响。BMA的目标是消除不同模块之间的所有代码依赖,以便我们的编码可以更高效。**注意:并非所有项目都需要BMA。前提是项目业务领域不会改变得太频繁。换句话说,应用程序应该首先被分为一些稳定的模块,然后我们可以在其上使用BMA**。
使用示例项目作为样本。它包含4个业务模块:首页、商店、销售、商品。没有BMA,其模块依赖关系如下:[图片链接](Resource/arch-without-bma.png),如果使用BMA,其模块依赖关系如下:[图片链接](Resource/arch-with-bma.png)。您可以看到,所有业务模块之间没有代码依赖关系。它们只依赖于公共和中介模块。例如,销售模块不知道商品模块。它通过中介获取商品信息。如果商品模块在中介上注册自己,销售模块可以获取商品信息;如果没有注册,销售模块则无法获取商品信息,但销售模块仍然可以成功构建。换句话说,商品模块的API错误不会影响销售模块的开发。
用法
Bifrost通过两种方式消除不同模块之间的依赖关系:路由URL和远程API。路由URL常用于跳转到其他UI页面。而远程API用于非UI操作或复杂的数据传输。
安装
建议使用CocoaPods来安装Bifrost库。例如:
pod 'Bifrost'
路由URL
Bifrost可以将一个URL字符串与一个块绑定起来。简单地,我们可以在+load方法中进行绑定。
//In GoodsDetailsViewController.m
+ (void)load {
//static NSString *const kRouteGoodsDetail = @"//goods/detail";
//static NSString *const kRouteGoodsDetailParamId = @"id";
//Above router url and param id are defined somewhere to avoid hardcoding
[Bifrost bindURL:kRouteGoodsDetail toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
GoodsDetailsViewController *vc = [[self alloc] init];
vc.goodsId = parameters[kRouteGoodsDetailParamId];
return vc;
}];
}
要调用URL交互:
//static NSString *const kRouteGoodsDetail = @"//goods/detail";
//static NSString *const kRouteGoodsDetailParamId = @"id";
//Above router url and param id are defined somewhere to avoid hardcoding
NSString *routeURL = BFStr(@"%@?%@=%@", kRouteGoodsDetail, kRouteGoodsDetailParamId, goods.goodsId);
UIViewController *vc = [Bifrost handleURL:routeURL];
if (vc) {
[self.navigationController pushViewController:vc animated:YES];
}
Bifrost会解析URL中的参数,并将它们放在bindURL:toHandler:方法处理器参数的parameters中。对于复杂参数,如图片对象,我们可以使用以下方法:
/**
The method to handle URL with complex parameters and completion block
@param urlStr URL string
@param complexParams complex parameters that can't be put in the url query strings
@param completion The completion block
@return the returned object of the url's BifrostRouteHandler
*/
+ (nullable id)handleURL:(nonnull NSString *)urlStr
complexParams:(nullable NSDictionary*)complexParams
completion:(nullable BifrostRouteCompletion)completion;
上述方法中的completion参数用于执行路由URL的回调。它将作为带有键kBifrostRouteCompletion的参数放入parameters中。
远程API
同样,尽管路由URL可以满足大多数要求,包括带有复杂参数的要求,但不方便。因此,我们仍然需要使用远程API来直接调用方法。例如,商品模块提供以下服务:
//In GoodsModuleService.h
@protocol GoodsModuleService <NSObject>
- (NSInteger)totalInventory;
- (NSArray<id<GoodsProtocol>>*)popularGoodsList; //热卖商品
- (NSArray<id<GoodsProtocol>>*)allGoodsList; //所有商品
- (id<GoodsProtocol>)goodsById:(nonnull NSString*)goodsId;
@end
@protocol GoodsProtocol <NSObject>
- (NSString*)goodsId;
- (NSString*)name;
- (CGFloat)price;
- (NSInteger)inventory;
@end
(上述声明位于 Mediator 项目的 demo 目录下的 GoodsModuleService.h 文件中。)商品模块需要实现一个 GoodsModule 类来遵循上述 GoodsModuleService 声明并提供实现。该 GoodsModule 类还应符合 BifrostModuleProtocol 协议,以便可以被 Bifrost 识别。商品模块还应在 +load 方法中简单地注册自己。
@implementation GoodsModule
+ (void)load {
BFRegister(GoodsModuleService);
}
...
@end
然后可以像这样调用 GoodsModuleService 中的那些 API
//In file ShoppingCartViewController.m in the demo
- (CGFloat)totalPrice {
CGFloat totalPrice = 0;
for (ShoppingCartItem *item in self.shoppingCartItemList) {
id<GoodsProtocol> goods = [BFModule(GoodsModuleService) goodsById:item.goodsId];
totalPrice += goods.price * item.num;
}
return totalPrice;
}
更多内容
整个项目的架构
BMA 库(Bifrost)只是业务模块化架构的一部分。更多的工作是对项目架构进行重构以符合 BMA 要求:不同的业务模块之间不能相互依赖代码。以下是在 demo 项目中使用的架构建议: App 中有 3 个部分:业务模块、公共模块 和 中介者。每个业务模块有 2 个目标:静态库用于代码,以及包用于资源。业务模块将其所有的公共 API 和路由 URL 放入一个 ModuleService 文件中。这个 ModuleService 放在 中介者 项目中,这样其他业务模块可以看到这些声明。业务模块为其 ModuleService 提供实现。您可以在 demo 项目中找到更多细节。
性能
您可能担心启动性能,因为我们把很多注册代码放在了 +load 方法中。实际上,Bifrost 的 +load 方法中的代码非常简单。我测试了注册 10000 个路由 URL 和 100 个模块,仅耗时 60ms。
//Get App pre-main time by Xcode's DYLD_PRINT_STATISTICS settings
//Without test code
Total pre-main time: 344.82 milliseconds (100.0%)
dylib loading time: 171.59 milliseconds (49.7%)
rebase/binding time: 36.06 milliseconds (10.4%)
ObjC setup time: 102.27 milliseconds (29.6%)
initializer time: 34.74 milliseconds (10.0%)
//With test code to register 10000 router urls and 100 modules
Total pre-main time: 366.12 milliseconds (100.0%)
dylib loading time: 179.28 milliseconds (48.9%)
rebase/binding time: 29.32 milliseconds (8.0%)
ObjC setup time: 63.77 milliseconds (17.4%)
initializer time: 93.50 milliseconds (25.5%)
//Note: the +load method mainly affects the initializer time.
如果您仍然想节省 60ms,您可以在应用启动后把绑定代码放到某些地方。
为什么我们需要路由 URL?
远程API似乎比路由URL更强大。为什么不用仅使用远程API呢?比如,阿里蜂巢库只提供对远程API的支持。主要原因有时我们需要一种可以在其他平台使用的方式,比如h5页面和安卓。直接使用URL跳转到另一页非常方便。因此,Bifrost也支持路由URL。