如何写出牵一发而动全身的代码

作者: e8b6cbadf7fb | 来源:发表于2018-03-30 22:29 被阅读14次

原文链接:https://shinancao.cn/2018/03/18/Project-Design-5/

工作中经常会听到这样的声音,“时间赶,先实现了再说,后面找时间再慢慢优化”。扪心自问吧,一个版本开发结束后,除了要改bug,你有多少次回头再去看写过的代码,可能早忘了你要优化什么了。就算你有心要优化,你还要去争取测试资源,如果不是一个部门的就更有难度了。再者,如果实现功能的过程中不注重细节和设计,只图快,那么到了测试阶段发现问题,或者后面的版本需求变动,改起来同样耗时多,还有可能因为缝缝补补让代码变得更加糟糕。唉,小伙伴们如果有似曾相识的经历,欢迎文末留言补充。

什么都放在ViewModel里,反正是MVVM

对于MVVM方式来构建代码,我们通常会封装一个ModuleViewModel,然后由它拉取数据,保存数据源,生成cell对应的viewModel,这里要说的被膨胀的viewModel就是ModuleViewModel。它要去调用接口、按照业务逻辑组装数据、生成cellViewModel、返回渲染tableView需要的东西,然后它里面可能又被接着放点击事件的跳转、分享逻辑、甚至是埋点相关的......因为它离数据源是最近的,所以把东西放在这里,处理起来是最方便的。但是这个viewModel也就变得越来越臃肿,非常难维护,更不要说可测试性和可复用性了。

给viewController瘦身没错,那也不能把所有东西都转移到ModuleviewModel中,或其他某个地方。所以要怎么做呢?我建议的做法是分离、封装,把一类事件的处理从viewModel或viewController中分离出去,如果跟某个业务相关的就封装成一个helper,如果是所有业务可以通用的就封装成一个manager。最终达到的目的就是让viewMoodel或viewController成为一个调度中心。

保存数据源的地方,在实现功能的过程中,如果不多加思考,代码量很容易就变得越来越庞大。这样的代码几乎就是一次性的,后面的人在接手时,为了改一个小地方,要把前前后后所有的逻辑都缕清楚,然后改完了还怕牵扯到什么其他的功能。

与具体的DataModel相关的逻辑,都散落在外面

再来说一下用来刷新某一块view的viewModel,比如cellViewModel。它不是简单的数据结构呀,它里面也可以放方法!它的职责其实是把从服务器拿回来的数据,转换成UI所需要的。所以这个处理过程不要放在生成它的地方,也不要放在view的updateUI中。比如从服务器拉取到的是商品的状态,然后根据商品状态button展示不同的title。

//写法一 处理过程放在UI中
@interface XXCellViewModel
@property (nonatomic, assign) kGoodsState state;
@end

@implementation XXCell

- (void)updateUI:(XXCellViewModel *)viewModel {
    swith(viewModel.state) {
    case kGoodsStateBuy:
        [self.btn setTitle:@"购买" forState:UIControlStateNormal];
        break;
    case kGoodsStateCollect:
        [self.btn setTitle:@"收藏" forState:UIControlStateNormal];
        break;
    ...
    }
}

@end

//写法二 处理过程放在生成cellViewModel的地方
@implementation XXPageViewModel

- (void)transformServerModel:(ServerModel *)serverModel {
    XXCellViewModel *cellModel = [[XXCellViewModel alloc] init];
    swith(serverModel.state) {
    case kGoodsStateBuy:
        cellModel.btnTitle = @"购买";
        break;
    case kGoodsStateCollect:
        cellModel.btnTitle = @"收藏";
        break;
    ...
    }
}

@end

//写法三 处理过程放在UI对应的viewModel中
@interface XXCellViewModel
@property (nonatomic, copy) NSString *btnTitle;
@end

@implementation XXCellViewModel
- (instancetype)initWithServerModel:(ServerModel *)serverModel {
    swith(serverModel.state) {
    case kGoodsStateBuy:
        _btnTitle = @"购买";
        break;
    case kGoodsStateCollect:
        _btnTitle = @"收藏";
        break;
    ...
    }
    //如果处理过程比较耗时,也可以用懒加载,重写get方法
}
@end

@implementation XXCell

- (void)updateUI:(XXCellViewModel *)viewModel {
    [self.btn setTitle:viewModel.btnTitle forState:UIControlStateNormal];
}

@end

显然第三种写法,是最好维护的,也是最好测试的。cellViewModel要为UI展示做好准备,而这个处理过程最好封装在其内部。

一个方法里处理不止一件事

如果你想让你的代码只有你一个人能搞懂,那这是个极好的技巧,这么干,说不定有一天你自己都不明白了。在一个方法里处理不止一件事,我总结大致有两种情况,一种是几件事有一个共同的处理逻辑,于是放在了一个方法里,把不同的部分用if else区分。一种情况是你处理着某个逻辑呢,正好顺便把其他的事情也一起干了,不知不觉就干了几件事。

//第一种情况
- (void)doSomethingWithType:(SomeType)type {
    NSString *str = @"";
    if (type == SomeType1) {
        str = @"123";
    } else if (type == SomeType2) {
        str = @"456";
    } else {
        ...
    }
    
    //use `str` to do the common things
}

//第二种情况,比如获取缓存数据
- (NSArray *)getCachedData {
    if (currentUserId == cachedUserId) {
        return cachedData;
    } else {
        [self clearCachedData];
    }
}

针对第一种情况,可以将共同的部分封装成一个方法,留出参数让调用者去传。不同的情况封装成不同的有针对性的方法,然后使用时,在各自情况处调用各自的方法。不要嫌麻烦,你应该尽可能减少if else的出现。

对于第二种情况,正如你的方法名一样,它是要干什么的就只干什么,不要偷偷的又做了其他事情。像上面例子中的,如果要清除之前登录过的用户的数据,可以选择其他时机处理,比如登录之后,或者初始化时比对一下。如果非要做两件事,那要把方法名取好,以免误导调用者。

方法用NSDictionary包装不同的参数

这也是一种非常容易把人搞的一头雾水的办法,你的dictionary里放的什么,如果没有注释,不看方法实现过程,别人根本猜不到。用代码来展示一下这种情况吧.

- (void)doSomethingWithDict:(NSDictionary *)dict {

    if ([dict[@"type"] isEqualToString:@"1"]) {
        NSString *param1 = dict[@"param1"];
        //use param1 to do something here
    } else if ([dict[@"type"] isEqualToString:@"1"]) {
        NSString *param2 = dict[@"param2"];
        //use param2 to do another thing here
    } else {
        ....
    }
    
    // do the some common things
}

这种情况其实也可以像上面提到的第一种情况那样,将该方法进行拆分,如果觉得实在没必要或者只能这样做,也请把方法的参数都有啥一一在方法定义中列清楚,有type时,用enum定义好。

弄一个魔法全局变量,在几个方法中改变其值

如果是一个全局的BOOL变量,为了做某种情况的标识,在几个方法中改变其值,是可以理解的。但是如果弄一个变量用来统计计数,你还在好几个方法中去改变其值就比较糟糕了。这让别人看你代码时会很蒙,都没办法预料到你在哪还会改变它的值。这些方法变得不够纯粹,如果不小心被调用了多次,那伴随着这个魔法值也有可能被计算错。

如果发生了这种情况,请自检一下整体解决问题的思路是否合理,还能不能通过其他手段来得到这个统计值,以及这个值存在的必要性。

不管界面由几部分数据源组成,都只用一个Section

可以有100种方法,把设计稿上的样式开发出来,但我们仍然要去思考其最优解。项目中常用到的就是tableViewcollectionView,如果你不管界面是由几部分数据源组成的,都只用一个section去做,有时往往会把事情变得很复杂。

  • 可能要把本可以作为sectionHeadersectionFooter的部分包装成cell,而这些cell可能根本不需要数据来刷新,展示的内容是死的。
  • 两个部分的model不同,所以dataSourceArray只能存id类型,再用的时候要一直去if else来判断。
  • 由于数据源来自不同的接口,为了保证dataSourceArray中的顺序,要把不同的接口有顺序的去请求,但是如果是分开的section,就可以回来一部分数据刷新一部分界面了。
  • 在做曝光买点时,涉及到一些计算,由于不知道每一块数据在dataSourceArray什么位置,又增加了计算的复杂度。
  • .....

如果界面是由几部分不同数据源组成的,多数情况下还是用不同的dataSourceArray来存储,界面用不同的section去做。

为了方便调用,随意引用

尽量去保证数据是单向流动的,引用也应该尽量保证是单向的,如果为了方便调用,随意引用,很可能造成你中有我我中有你的循环import。比如在moduleviewModel中去引用viewController或者在cellViewModel中去引用cell,那一定是有办法可以消除这种情况的。一旦互相引用,两者强绑定在一起,那都没办法去复用了。

滥用RACCommand

我觉得RAC里最妙的就是Signal,它让我们省去了写delegateblocknotification的一堆代码。当然了,它还有很多像mergecombine这样的操作。所以RAC范畴内的东西,几乎都会和Signal有关,包括RACCommand。咱们先来看一个RACCommand正常来使用的姿势,然后阐明这里所说的“滥用RACCommand”。

self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
   return [client logIn];
}];

[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

self.loginButton.rac_command = self.loginCommand;

RACCommand用一个Signal来对其初始化,在调用execute时也返回了Signal,可以对其监听执行的结果,然后把这个command跟button绑定在一起,当button被点击时就会去执行这个command。当然了,也可以手动调用RACCommand。可是你如果写成了下面这样......

@interface XXViewModel()
@property (nonatomic, strong) RACCommand *subscribeCommand;
@end

@implementation XXViewModel
- (instancetype)init {
    if(self = [super init]) {
        _subscribeCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
            [self doSubscribe];
            return [RACSignal empty];
        }];
    }
    return;
}

- (void) doSubscribe {

}
@end

//在调用的地方
[viewModel.subscribeCommand excute:nil];

看出问题了吗?这样用,跟我直接调用[viewModel doSubscribe]有什么区别!在viewModel中把要执行的方法都这样用RACCommand包装一下,看似用RAC,实际它的一点简便性都没用到。

几点不成熟的小建议

我们很多人在工作中可能更多的时候都是在做业务功能,业务一复杂,代码的复杂度也上来了,而且业务还是经常容易变更的,所以我们要尽可能的把代码写的清晰明了。几点不成熟的小建议吧:

  • 从整体中分离出去,把一类的东西进行封装,大到层与层之间,小到一个方法的实现。还是那句老话,高内聚、低耦合。
  • 避免if else的层级嵌套,一旦多了,就说明有可能你在一个方法块中处理了很多事情。
  • 不要怕多写几个类,多写几个方法,其实就按照逻辑本身的样子一点点往下走就行。
  • 不要去copy、paste,说出来都知道,可就是控制不住自己的手呢。

最近在看一本叫《股票作手回忆录》的书,里面有这样一个注解:

逆反行为和从众行为一样愚蠢。我们需要的是思考,而不是投票表决。不幸的是,伯特兰罗素对于普通生活的观察又在金融界中神奇地应验了:“大多数人宁愿去死也不愿意去思考。许多人真的这样做了。” ————巴菲特

--End--

相关文章

  • 如何写出牵一发而动全身的代码

    原文链接:https://shinancao.cn/2018/03/18/Project-Design-5/ 工作...

  • 怎样写出好代码

    怎样写出好代码 【指南】如何写出好代码 代码就是设计(Jack W.Reeves, 1992) 代码是最有价值的交...

  • 代码编写注意事项

    如何写出好代码,这个是一个值得考虑的问题。怎样才能写出即可读又高效的代码呢? 本文从编码的细微处入手,总结如何写出...

  • 倔强青铜:如何避免写出丑陋的通知代码

    倔强青铜:如何避免写出丑陋的通知代码

  • 如何写高性能的JS

    我们前面讲述都是编写的代码导致内存泄露如何去排查,但是如何去写出高性能的JS代码 如何精准测试JavaScript...

  • 如何写出优雅的代码?

    本文仅仅是对《代码整洁之道》摘录: 简单代码,重要顺序:1.能通过所有的测试2.没有重复代码3.体现系统中的全部设...

  • 如何写出好的代码???

    问题 1.在阅读大牛写的代码时候,有没有觉别人的代码写的比我们自己好?2.在读有些人的代码时候,有没有发现完全读不...

  • 如何写出艺术的代码

    1、命名 2、注释 3、减少流程控制 4、一段代码只做一件事 5、写出简洁的代码 6、通过提前返回来简化逻辑控制 ...

  • 如何写出干净的代码?

    如何写出干净的代码? 首先来说什么是干净的代码? 干净的代码客观上说,程序复杂度一定是很低的。 干净与不干净虽然本...

  • 如何写出不好的代码

    [toc] 如何写出不好的代码 这是一个从各个项目整理出来的代码,具有一定代表性。坏代码的产生,本篇暂时不深究原则...

网友评论

本文标题:如何写出牵一发而动全身的代码

本文链接:https://www.haomeiwen.com/subject/oujwcftx.html