26

一种简单的iOS 的组件化设计

 5 years ago
source link: http://www.cocoachina.com/ios/20180726/24337.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言:如今iOS的开发中,组件化设计都成为一种标配了,要是不用上好像就赶不上潮流了。网上相关的方案分析也多如牛毛。分析比较多的是URLRoute / 蘑菇街的 url-block 和 Protocol-Class / 和 casa 的target-action。

一、 方案比较

URLRoute 与蘑菇街 url-block 和Protocol-Class 都不得不维护一套 url 或 Protocol 与组件间的映射,通过 url 或 Protocol 去寻找组件。先不说效率问题,单是编写和维护这个映射的代码,使用者使用必须先注册,然后才能使用,模板代码会充满整个项目。其实小的觉得这份映射是完全没必要的。

casa 的target-action 不用维护映射表,这个方案使用 “Target_ + 组件名字” 作 为key 通过runtime 寻找服务。但是 hardcode 的地方太多,宏定义或者const 变量充满了头文件。这样使用起来必须记忆很多const变量,编码效率也稍微下降。在调用组件的时候,cache 了 targetClass。不晓得是否觉得NSClassFromString 方法不够高效。

二、小的试把两套方案结合起来,弄一个叫protocol-module模式的组件化

1.约定

oc 语言中充满了约定。比如 类的初始化方法 initXXX,系统合成的setter getter方法。更具体的是KVO观察了一个类的属性,被观察的类变更为一个带约定前缀的子类

- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;

ryuAZvi.png!web

image.png

小的这套方案最主要的是编写代码的约定,思路如下图

aua6zqb.png!web

image.png

2.说明

小的打个比方。我们把主app称为用户,用户委托商家生产一个组件。用户把需求和想怎么使用这个组件告诉商家。“需求”的实现就是组件本身,“怎么使用”对应的就是协议了。因为协议是双方都知晓的,所以是公开的。这样的好处是,用户不管组件是怎么实现的,我只要按协议使用就行了。

图中public protocol 是定义好的组件调用协议,基本协议规定了组件回调的 callback,和服务体 serverBody,也就是组件提供服务的 controller。组件moduleA 、moduleB等为了使用者方便,都会实现一个interface的类。这个类遵守对应的协议,类名必须是 “协议名 + SI”。比如协议名是ModuleA,那么interface类的类名就是ModuleASI。

1)编写协议和组件

基本协议定义如下

#ifndef MoudleProtocol_ServerInterface
#define MoudleProtocol_ServerInterface @"SI"
#endif
@protocol BaseModule 
<nsobject>
 
@required
// server body
@property(nonatomic, weak) __kindof UIViewController *serverBody;
@optional
// callback
@property(nonatomic, copy) void (^callback) (id params);
@end
</nsobject>

通过将 BaseModule 协议子协议化,定制特定组件调用协议。例如我们要调用组件moduleA,我们可以定义一个叫ModuleA的协议,这个协议遵守BaseModule协议

@protocol ModuleA 
<basemodule>
 
@required
// input
@property(nonatomic, strong) NSString *name;
@end
</basemodule>

这个协议很简单,只有一个属性name,指定了我们调用组件时要传入的参数是name。

定好了协议,商家生产出组件后,他还要做一件事就是实现interface接口类ModuleASI

这个类实现如下

@interface MouduleASI : NSObject 
<modulea>
 
@end
@implementation MouduleASI
- (UIViewController *)serverBody {
  if (!serverBody) {
    UIStoryboard *sb  = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    serverBody = [sb instantiateViewControllerWithIdentifier: @"OhgRacViewController"];
    ((OhgRacViewController *)serverBody).interface = self;
  }
  return serverBody;
}
@end
</modulea>

好了,代码很简单不是吗,商家不会抱怨让他多干活的。在MouduleASI的.m文件里,商家简单的返回一个serverBody。

2)大功告成了吗?还差一点

组件化最重要的路由忘了。不过放心,小的这个路由很简单的,老规矩,先看代码,路由定义

@interface OHGRouter : NSObject
+ (instancetype) router;
- (id) interfaceForProtocol:(Protocol *) p;
- (id) interfaceForURL:(NSURL *) url;
// for unit test
- (void) assertForMoudleWithProtocol:(Protocol *) p;
- (void) assertForMoudleWithURL:(NSURL *) url;
// navi type for vc
// push present
- (UIViewController *) findVcOfView:(UIView *) view;
@end

同样简单的要死,小的不用说各位看官都基本明白了,不过,还是啰嗦一下...别打脸

  • (instancetype) router;这个方法略

  • (id) interfaceForProtocol:(Protocol *) p;这个方法是通过协议找到对应的组件接口类,实现如下

- (id)interfaceForProtocol:(Protocol *)p {
  Class cls = [self _clsForProtocol:p];
  return [[cls alloc] init];
}
- (Class) _clsForProtocol:(Protocol *) p {
  NSString *clsString = [NSStringFromProtocol(p) stringByAppendingString: MoudleProtocol_ServerInterface];
  return NSClassFromString(clsString);
}

好吧,就这么点代码,获取协议名,拼接上前面public protocol的宏定MoudleProtocol_ServerInterface @"SI",就是我们的接口类名。这就是我们为啥提前做了约定的原因。(类似苹果实现的KVO),这个约定好处在于,我们完全不用维护 url-block / protocol-class / target-action等在内存的映射,也就不需要注册组件了。

  • (id) interfaceForURL:(NSURL *) url;这个方法可以处理远端调用组件,我们稍后再说

// for unit test

  • (void) assertForMoudleWithProtocol:(Protocol *) p;

  • (void) assertForMoudleWithURL:(NSURL *) url;

    这两个方法很有意思,在单元测试的时候,我们容易通过协议和url检查,组件是否实现了 interface 接口类,看代码

- (void)assertForMoudleWithProtocol:(Protocol *)p {
  if (![self _clsForProtocol:p]) {
    NSString *protocolName = NSStringFromProtocol(p);
    NSString *clsName = [protocolName stringByAppendingString: MoudleProtocol_ServerInterface];
    NSString *reason = [NSString stringWithFormat: @"找不到协议 %@ 对应的接口类 %@ 的实现", protocolName, clsName];
    [self _throwException: reason];
  }
}

如果我们找不到接口类,说明组件还没写好,抛出异常。

以上,路由也就写好了。

3)很快就完成了,看看怎么使用

在ViewController.m下

#import "ViewController.h"
#import "OHGRouter.h"
@implementation ViewController
- (IBAction)showModlue:(id)sender { 
  id 
<modulea>
  obj = [[OHGRouter router] interfaceForProtocol: @protocol(ModuleA)];
//  id
 <modulea>
   obj = [[OHGRouter router] interfaceForURL:[NSURL URLWithString:@"ModuleA://?name=xiaobaitu"]];
  obj.name = @"小白兔";
  obj.callback = ^(id params) {
    NSLog(@"%@", params);
  };
  [self.navigationController pushViewController: obj.serverBody animated: YES];
}
@end
 </modulea>
</modulea>

在showModlue方法里,小的简单的传入协议ModuleA,就能调用组件了,传入参数name, 实现回调callback。并且咱基本上没有hardcode,所以还有编码过程编译器还能给温馨提示。在整个ViewController.m,咱并没有导入任何组件相关的信息,路由里也没有,很简单的咱就实现了组件的解耦。这一切就是因为我们一开始就明确了协议和组件接口的约定。

4)还没完

前面说到路由- (id) interfaceForURL:(NSURL ) url;方法,从使用就可以看出来这事怎么回事了id obj = [[OHGRouter router] interfaceForURL:[NSURL URLWithString:@"ModuleA://?name=xiaobaitu"]];

只要我们按格式| 协议名://参数 *| 调用就大功告成了,例如: ModuleA://?name=xiaobaitu。

好了,完了

作者:沉思者Tiebreaker

链接:https://www.jianshu.com/p/15ac855771a5


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK