iOS Command模式的一次实践

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

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

值得改进的点<a id="orgheadline1"></a>

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

解决和改进方案<a id="orgheadline6"></a>

目录结构<a id="orgheadline2"></a>

  • WTCommand commad类
  • WebCommands 工厂类

现在手上的代码<a id="orgheadline3"></a>

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来执行请求。后面我们会将这部分逻辑从业务逻辑层分离出来。

使用工厂类封装接口<a id="orgheadline4"></a>

改进的第一步是首先创建工厂类,通过提供参数的方式来使业务逻辑层获得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<a id="orgheadline5"></a>

再看一下我们的代码,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"}];

设计模式<a id="orgheadline7"></a>

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,608评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,646评论 18 139
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,182评论 1 23
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,980评论 25 707
  • 小张和小王一起参加面试,成功的进去了公司的销售部门。 他们进行初步培训后,开始工作。小张认为先熟练掌握好基...
    梅韵Eva阅读 213评论 0 2