iOS Command模式的一次实践

注:本文参考 记一次网络模块的小规模重构 以及 MVVM without ReactiveCocoa
看完本文后,如果需要代码的话,请移步https://github.com/tepmnthar/WTCommand

就从这两篇文章结束的的地方说起,这两篇文章提供了一个很好的模版和思路,但仍有一些值得改进的地方。

值得改进的点

  1. 不需要将error和result暴露给下游环节,对于下游环节来说,只需要能将输入和输出回调交给指定command就行了,并不需要其来判断究竟该执行哪个回调,这个操作应该由更上层的来完成。
  2. 所以为了实现上面这个要求,并且进一步的分离业务逻辑层和调用请求层,需要在这中间额外加一个中间层,用来控制请求路由和控制成功失败回调。本文以网络请求为例,即在中间层使用工厂类创建所需command,并且将网络请求结果放进对应的成功或失败回调,下游的业务逻辑层无需再做判断
  3. 其他一些功能,诸如监视command状态变化;final回调;类似其它语言的点表达式之类。

解决和改进方案

目录结构

  • WTCommand commad类
  • WebCommands 工厂类

现在手上的代码

WTCommand

typedef enum : NSInteger {
    COMMAND_IDLE,
    COMMAND_PENDING,
    COMMAND_SUCCESS,
    COMMAND_ERROR,
    COMMAND_CANCEL
} CommandStatus;

-(instancetype)initWithConsumeHandler:(WTCommandConsumeBlock)consumeHandler successHandler:(WTCommandSuccessBlock)successHandler errorHandler:(WTCommandErrorBlock)errorHandler{
    self=[super init];
    if (!self) return nil;
    _status=COMMAND_IDLE;  //unused
    _consumeHandler=consumeHandler;
    __weak typeof(self)weakself=self;
    _successHandler=^(id result){
        __strong typeof(self)strongself=weakself;
        strongself.status=COMMAND_SUCCESS;
        successHandler(result);
    };
    _errorHandler=^(id error){
        __strong typeof(self)strongself=weakself;
        strongself.status=COMMAND_ERROR;
        errorHandler(error);
    };
    return self;
}

-(void)execute:(id)input{
    if (_status==COMMAND_PENDING)return;
    _status=COMMAND_PENDING;
    _consumeHandler(input);
}

开头介绍的两篇文章读完后,代码差不多应该是这样的。创建command时设置command的请求函数和失败成功回调函数,通过execute来执行请求。后面我们会将这部分逻辑从业务逻辑层分离出来。

使用工厂类封装接口

改进的第一步是首先创建工厂类,通过提供参数的方式来使业务逻辑层获得command,而不是业务逻辑层直接去操作command。直接在业务逻辑层中创建command会造成代码臃肿,实际上业务逻辑层总是在调用那么几个接口,仅仅是设置的参数不一样,回调函数不一样,所以可以抽象一下,业务逻辑层仅仅只关心要执行哪个command,输入是什么,输出怎么处理,仅此而已。
而工厂类则负责根据业务逻辑层提供的参数,去装配出指定的command,请求哪个接口,以及把返回结果放进哪个回调,全部由工厂类内部完成。工厂类实际起到了路由的功能,负责request和response的收发。
创建工厂类的另外一个好处就是,代码逻辑都写在一个类里,代码很好复用。如果要换请求接口,比起修改混杂在业务逻辑中的command的做法,要容易许多。如果回调可以复用,甚至可以把回调函数也写在里面,这样业务逻辑层里代码就只有一句话,非常简洁。
这里以网络请求为例,创建WebCommand类:
WebCommand.m
定义command类型:

typedef enum {
    WEB_COMMAND_NONE,
    WEB_COMMAND_TEST_GET,
    WEB_COMMAND_TEST_POST
}WEB_COMMANDS_TYPE;

创建对应command:

+ (WTCommand*)constructCommandFromCommand:(WEB_COMMANDS_TYPE)type successHandler:(WTCommandSuccessBlock)successHandler errorHandler:(WTCommandErrorBlock)errorHandler{
    WebCommands* ret=[[WebCommands alloc] init];
    if (!ret) return nil;
    switch (type) {
        case WEB_COMMAND_TEST_GET:{
            return [ret constructTestGetDataCommandSuccessHandler:successHandler errorHandler:errorHandler];
            break;
        }
        case WEB_COMMAND_TEST_POST:{
            return [ret constructTestPostDataCommandSuccessHandler:successHandler errorHandler:errorHandler];
            break;
        }
        default:
            return nil;
            break;
    }
}

因为我们在业务逻辑层和网络请求层多加了一层,我们在这层中设置网络请求调用的接口和路由,并且可以将返回结果放进指定回调,不必再由下游的业务逻辑层来做判断。

-(WTCommand*)constructTestGetDataCommandSuccessHandler:(WTCommandSuccessBlock)successHandler errorHandler:(WTCommandErrorBlock)errorHandler{
    __block WTCommand* command=[[WTCommand alloc]initWithConsumeHandler:^(id input) {
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        [manager GET:@"http://www.example.com" parameters:input progress:nil success:^(NSURLSessionTask *task, id responseObject) {
            NSLog(@"JSON: %@", responseObject);
            command.successHandler(responseObject);
        } failure:^(NSURLSessionTask *operation, NSError *error) {
            NSLog(@"Error: %@", error);
            command.errorHandler(error);
        }];
    } successHandler:successHandler errorHandler:errorHandler];
    return command;
}

command的使用:
这样我们在业务逻辑层使用command的方式就变成了这样

// 通过工厂类创建command
WTCommand* command=[WebCommands constructCommandFromCommand:WEB_COMMAND_TEST_GET successHandler:^(id result) {
    NSLog(@"result in WebCommand callback:: %@",result);
} errorHandler:^(id error) {
    NSLog(@"error in WebCommand callback:: %@",error);
}];
// 设置输入参数并执行
[command excute:nil];

状态监控和final

再看一下我们的代码,command内部有状态的改变,但却没有一个统一的方法能设置状态改变后的回调,这就很不合适。
WTCommand

-(instancetype)initChangeStatusCallback:(WTCommandChangeStatusCallback)changeStatusCallback{
    if (changeStatusCallback){
        _changeStatusCallback=changeStatusCallback;
    }
    return self;
}

在每次改变状态后都调用一次changeStatusCallback即可

因为业务逻辑层只能执行成功回调或者失败回调中的一种,如果我们需要在无论失败成功时都执行一段代码的话,就必须在成功回调和失败回调里插入final回调。做法同上,通过函数设置final回调,在生产command对象时将本来的成功失败回调替换,插入final回调。

-(instancetype)initChangeStatusCallback:(WTCommandChangeStatusCallback)changeStatusCallback{
    if (changeStatusCallback){
        _changeStatusCallback=changeStatusCallback;
    }
    return self;
}

-(instancetype)initWithConsumeHandler:(WTCommandConsumeBlock)consumeHandler  successHandler:(WTCommandSuccessBlock)successHandler errorHandler:(WTCommandErrorBlock)errorHandler cancelHandler:(WTCommandCancelBlock)cancelHandler{
// other stuff
    _successHandler=^(id result){
        __strong typeof(self)strongself=weakself;
        strongself.status=COMMAND_SUCCESS;
        successHandler(result);
        strongself.changeStatusCallback(COMMAND_SUCCESS);
        strongself.finalHandler(result);
    };
    _errorHandler=^(id error){
        __strong typeof(self)strongself=weakself;
        strongself.status=COMMAND_ERROR;
        errorHandler(error);
        strongself.changeStatusCallback(COMMAND_ERROR);
        strongself.finalHandler(error);
    };
// other stuff
}

这里两个初始化函数的返回都是instancetype,主要是我比较喜欢其它语言比如javascript的一路点下去的写法。
所以整个的用法是这样:

[[[[WebCommands constructCommandFromCommand:WEB_COMMAND_TEST_POST successHandler:^(id result) {
    NSLog(@"result in WebCommand callback:: %@",result);
} errorHandler:^(id error) {
    NSLog(@"error in WebCommand callback:: %@",error);
}] finalHandler:^(id output) {
    NSLog(@"final in WebCommand callback:: %@",output);
}] initChangeStatusCallback:^(CommandStatus status) {
    switch (status) {
        case COMMAND_SUCCESS:
            NSLog(@"successStatus");
            break;
        case COMMAND_ERROR:
            NSLog(@"errorStatus");
            break;
        case COMMAND_PENDING:
            NSLog(@"pendingStatus");
            break;
        default:
            break;
    }
}] excute:@{@"mobile":@"12345678",@"passwd":@"123456"}];

设计模式

最后来谈下设计模式,这里大概用上了command和工厂模式。事实上我并不是特别熟设计模式,但写代码时只要经常想着怎么把代码写的扩展性好、耦合度低,代码写的合理、优雅,模式自然而然会出现,当然经常借鉴和学习也是很重要的。

你可能感兴趣的:(iOS Command模式的一次实践)