组件化这个词相信大家已经听过很多次了,其实就是将一个复杂的系统进行拆分一个个更小的组件(或模块,虽然网上有文章讲了两者的区别,但是我实在是看不出什么区别,既然组件化已经深入人心,我们就放宽心用这个词吧),说到拆分我们会想到单一职责、高内聚底耦合,这确实是拆分组件所需要达到的目的,但也不只这些目的,比如我们做组件化需要解决的问题是,快速开发生码SDK,能够复用代码,快速替换生码页,快速替换支付方式页,或则是快速裁剪某个业务组件,这些又引出了几个组件化的目的,提高代码复用性,可插拔,可快速替换业务组件,正是因为这些目的有了这次组件化的实践。
组件应该有自己所属的架构层级,为了更形象这里我用垂直分层这一词,基于川航SDK做组件化拆分我把垂直层级划分为三层如下:
-
越上层越和业务相关、越容易变化、越不应该直接耦合
-
越下层越通用(与业务无关),越稳定,越容易直接依赖
-
下层组件可以被上层组件直接依赖
-
同层级组件除了独立业务层里的组件不能直接依赖,通用业务层里的组件及基础组件层里的组件都可以直接依赖
基础组件层的组件可以直接依赖,但依赖一定是单向的,根据业务层使用频率与功能职责进行横向划分,其实也可以完全按照功能职责进行划分,但我觉得这样划分粒度太细。
Core和UI组件可以运用到其它非生码SDK项目,WXApi、RSA其它SDK可能不会用到所以单独拆分成组件,Router如果SDK不需要组件化或则路由解析也可以不集成,Network我认为可能替换为像YTKNetwork这种离散式Api的网络请求所以单独拆分
- 开发基础组件层组件时一定要保证组件易用性,稳定性及可扩展性
- 该层级组件一定要遵循代码规范,因为该层组件是通用业务层和具体业务层都会且经常用到的,应当起到一个典范的作用让大家学习最终习惯
比如Network开发时需要让业务层通过继承或依赖注入定制或扩展,UI组件可以通过参数配置就可以满足调用方需求,而不用修改该层组件,代码示例如下:
//网络请求通用参数配置和加签插件注入,满足使用方定制不一样的参数配置逻辑及加签逻辑
ZYHMNetworkClient *client = [ZYHMNetworkClient weakSingleton];//弱引用单例与第一次创建时的持有者拥有相同的生命周期
client.baseURL = manager.config.baseURL;
[client addAccessory:({
ZYHMDefaulNetworkConfigAccessory *accessory = [ZYHMDefaulNetworkConfigAccessory new];
accessory.appID = manager.config.appId;
accessory.appKey = manager.config.appKey;
accessory.version = manager.config.version;
accessory.fetchToken = ^NSString *{//动态参数
return [ZYHMUserManager shared].userModel.token;
};
accessory;
})];
//自定义AlertController,尽量满足使用方定制化需求
typedef NS_ENUM(NSUInteger, ZYHMAlertActionStyle) {
ZYHMAlertActionStyleDefault,
ZYHMAlertActionStyleHighlight,
};
@class ZYHMAlertController;
typedef void(^ZYHMAlertControllerHandler)(ZYHMAlertController *alertVC);
@interface ZYHMAlertController : ZYHMPopZoomVC
- (instancetype)initWithTitle:(id)title
message:(id)message;
- (void)addActionWithTitle:(id)title
style:(ZYHMAlertActionStyle)style
handler:(ZYHMAlertControllerHandler)handler;
@property (nonatomic) UIView *customView;//自定义视图
@end
属于SDK业务层,可能被多个独立业务层组件依赖,这种依赖是直接依赖,不用采用组件间通信,垂直拆分该层是为了复用代码,该层为独立业务层服务(生码逻辑其实当前只被生码页依赖,但生码页的种类很多可能被扩展为多个组件),该层中的组件可以直接依赖,但也需要保证依赖的单向性,其实该层也可以拆分到独立业务层,但考虑到易用性(直接依赖)和代码复用我们拆分出了该层。
- UserManager负责用户数据管理,登录注册等接口管理
- SDKManager负责通用的SDK配置管理,初始化SDK及参数校验,由于下层不应该依赖与上层组件,所以里面初始化成功后跳转到服务开通页还是乘车页是由上层注入Block实现,该组件依赖UserManager组件来调用始化接口(登录接口),及判断用户的登录状态
- ErrorCode该组件包含了业务层接口的所有错误码及SDKManager与UserManager依赖的错误码,该组件被SDKManager与UserManager直接依赖
- QRCodeGenerator负责生码逻辑,因为不会被生码Module外的其它组件直接依赖本来可以和生码Module合并在一起,但是考虑到后期生码Module可以会扩展为多种生码方式的Module,因为将该组件单独拆分并下沉到通用业务层,方便多Module直接依赖及复用。由于该组件包含了GMSSL的静态库且静态库头文件中有
# include <openssl/opensslconf.h>
这样的导入形势,需要注意配置头文件搜索路径,该组件也依赖SDKManager来处理码种子数据 - CommonView|VC 包含的SDK业务相关的VC基类来提供右上角的关闭更多按钮,及常见问题,协议等VC,原本常见问题、协议等VC也可以拆分成单独的独立业务组件,可以通过依赖注入完成相关跳转,注入的方式有很多中比如属性注入,初始化参数注入,方法参数注入,但是要向ZYHMBaseSDKVC注入依赖需要考虑应该注入什么对象,协议对象,Block,Selector等,考虑到ZYHMBaseSDKVC有很多子类,如果想减少代码量避免每次示例化子类对象是都去注入,也可以通过注入一个类方法(组装SDK时添加分类)来实现只需要一次注入,当点击更多按钮时调用该类方法来实现弹窗逻辑。除了依赖注入我们也可以通过依赖查找来满足下层调用上层业务的情形,比如添加一个指定类名的类,添加一个指定route回调。但是这里因为常见问题,协议等VC变化不大只是入参URL可能变化,则可以直接依赖SDKManager中的配置数据来满足使用方需求,所以没有单独拆分。
对于通用业务层来说,它更接近上层业务,所以业务入侵更严重,同时还存在更多的相互依赖问题。这一阶段的重点工作就是梳理依赖关系,解除不合理的依赖,保证组件之间不能存在循环依赖。我们都需要通过依赖注入,依赖查找来消除循环依赖
这里举一个例子,我的的SDKManager依赖UserManager来做初始化操作及用户登录状态判断,起初UserManager直接依赖SDKManager还获取SDKConfig数据,我们做拆分后只需要将UserManager需要的数据通过方法注入注入其中就免除了对SDKManager的依赖,如果实在需要直接依赖SDKConfig参数,可以把SDKConfigModel和ErrorCode合并为通用业务数据组件来被SDKManager依赖UserManager直接依赖
上面的案例只是为了举例说明,实际过程中根据不同的业务场景使用不同的手段,需要从易实现,易维护,易扩展等方面去考虑。对于一些核心复杂模块,可能无法像上面描述的那样简单的去拆分,而是需要整体梳理重构去解耦,再比如某些模块已经确定后续不再维护,那我们甚至可以利用Runtime,Route等手段来解除依赖
该层是为了辅助业务组件间通信而存在的,里面定义了独立业务组件对外暴露的路由及常量,避免了组件间调用过多硬编码,因为该层组件比较轻量,业务层组件可以直接依赖(即便某个独立业务组件被拆除,依然依然需要依赖该组件提供的Route路由组件),比如行程组件,我们在SDK中可能不包含该模块(podfile中没有导入),但是我们对行程Route的依赖依然存在的,因此我们编译项目时才不会报符号未定义相关错误。
为什么我们的Route层位于通用业务层之下呢?我们上面说了通用业务层是因为易用性和代码复用存在的,因此它任然为业务层,虽然通用组件层不会直接依赖独立业务层,但是通用组件层可以通过依赖Route层来实现对独立业务层能力的访问比如:我们的SDKBaseVC里封装了更多按钮点击时相关逻辑,如果更多点击后的弹窗包含了一个跳转到行程页的逻辑,这时我们只需依赖行程Route便可以完成相关跳转
Route层类似于CTMediator(Target Action组件通信)中的组件Category,类似于BeeHive中的Protocol接口层,旨在隔离依赖,由组件方提供,方便依赖该组件的其它组件方便调用
这里列举了行程页Route:
//ZYHMJourneyRoute.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
extern NSString * const ZYHMJourneyRouteFetchMainVC;
NS_ASSUME_NONNULL_END
//ZYHMJourneyRoute.m
#import "ZYHMJourneyRoute.h"
NSString * const ZYHMJourneyRouteFetchMainVC = @"journey_vc";
Route中可以包含了供ZYHMRoute调用的ZYHMBybusRouteFetchMainVC路由常量字符串及通知监听的ZYHMBybusRouteNotificationRefreshQRCode通知名常量字符串
- 根据业务职责进行划分,只存放业务的具体实现代码,独立业务层如其名是独立的组件,可以同理其它同层组件独立运行
- 独立业务组件通过ZYHMRoute中间件和Route层提供的路由组件与其它独立业务组件进行组件间通信
- 独立业务层可以直接依赖通用业务层,Route层及基础组件层的所有组件,注意Podspec中进行依赖声明
- 禁止该层的组件与同层组件之间不能直接依赖(不能直接import)
这里列举服务开通Module调用乘车Module对外提供的能力:
**1.**声明ZYHMByBusRoute即定义Module提供能力对应的常量及外部需要直接使用的类似于通知名等常量
//ZYHMBybusRoute.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
extern NSString * const ZYHMBybusRouteFetchMainVC;
extern NSString * const ZYHMBybusRouteNotificationRefreshQRCode;
@interface ZYHMBybusRoute : NSObject
@end
NS_ASSUME_NONNULL_END
//ZYHMBybusRoute.m
#import "ZYHMBybusRoute.h"
NSString * const ZYHMBybusRouteFetchMainVC = @"bybus_vc";
NSString * const ZYHMBybusRouteNotificationRefreshQRCode = @"ZYHMBybusRouteNotificationRefreshQRCode";
@implementation ZYHMBybusRoute
@end
**2.**声明ZYHMByBusModule用来向ZYHMRoute中注册它所提供的能力(注意ZYHMByBusModule会直接依赖ZYHMByBusRoute)
//ZYHMByBusModule.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ZYHMByBusModule : NSObject
@end
NS_ASSUME_NONNULL_END
//ZYHMByBusModule.m
#import "ZYHMByBusModule.h"
#import "ZYHRoutes.h"
#import "ZYHMBybusRoute.h"
#import "ZYHMByBusVC.h"
@implementation ZYHMByBusModule
+ (void)load {
[[ZYHRoutes globalRoutes] addRoute:ZYHMBybusRouteFetchMainVC handler:^ZYHRouteResult *(NSDictionary<NSString *,id> * _Nonnull parameters) {
ZYHRouteResult *result = [ZYHRouteResult successResult];
result.extraData = [ZYHMByBusVC new];
return result;
}];
}
@end
**3.**服务开通组件要获取乘车页组件提供页面进行跳转
//ZYHMCityServiceOpenVC.m
...
- (void)openBtnClicked:(id)sender {//开通按钮被点击
if (!self.protocolView.isSelected) {
[ZYHMProgressHUD showError:@"请先阅读并同意相关协议"];
return;
}
[self.view.zyhm_hud showLoading];
[[ZYHMetroSDKManager shareInstance] registerUserSuccess:^(ZYHMUserModel * _Nonnull model) {
[self.view.zyhm_hud hideAnimated:YES];
ZYHRouteResult *result = [[ZYHRoutes globalRoutes] routeURL:[NSURL URLWithString:ZYHMBybusRouteFetchMainVC]];//获取乘车页
UIViewController *bybusVC = result.extraData;//调用路由同步会返回ZYHRouteResult类型对象,包含是否成功的标识,及附加信息
UIViewController *payTypeVC = [[ZYHRoutes globalRoutes] routeURL:[NSURL URLWithString:ZYHMPayTypeRouteFetchMainVC]].extraData;//获取支付方式页
[self.navigationController setViewControllers:@[bybusVC, payTypeVC] animated:YES];
} failure:^{
[self.view.zyhm_hud hideAnimated:YES];
}];
}
...