Bifrost 1.0.0

Bifrost 1.0.0

Jackie Yang 维护。



Bifrost 1.0.0

  • 作者
  • JackieYang

License MIT  CocoaPods  平台  支持  构建状态

有赞logo

项目logo

一款令人愉悦的应用业务模块化架构库。

中文版README

什么是 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 项目中使用的架构建议:Demo 中的 BMA 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。