# iOS开发之均衡代码职责浅谈

语言：zh-CN
原文：[iOS开发之均衡代码职责浅谈](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao68>)
中文：[中文 HTML](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao68>)
English: [English HTML](<https://allluckly.cn/en/journal/2016-10-27-balancing-code-responsibilities-in-ios/>)
发布于: 2016-10-27T00:00:00+08:00
内容更新于: 2016-10-27T00:00:00+08:00
署名：Sindri

围绕 MVC 中控制器过于庞大的问题，用项目示例讨论视图、模型和控制器的职责分配，提取重复逻辑并调整功能归属，强调在更换架构名称之前先理清代码责任。

原文署名Sindri，Bison 编辑收录。

[原文所列来源](<http://www.jianshu.com/p/92d086b8efd8>)

**阅读前** 这是 2015–2017 年间的技术记录，本次整理未重新运行验证示例。涉及的 API、依赖和工具版本请结合发表日期阅读。

MVC是软件工程中的一种软件架构模式，它把软件系统分为三个基本的部分：模型Model、视图View以及控制器Controller。这种模式的目的是为了实现一种动态的程序设计，简化后续对软件系统的修改和扩展，并使得程序的某一部分的复用成为可能。

编辑：[Bison](<https://allluckly.cn/>)  
 来源：[Sindri的小巢](<http://www.jianshu.com/p/92d086b8efd8>)

文章的标题有点绕口，不过想了半天，想不到更好的标题了。本文的诞生有一部分功劳要归于[iOS应用现状分析](<http://www.jianshu.com/p/a51d66383eb9>)，标题也是来源于原文中的“能把代码职责均衡的划分到不同的功能类里”。如果你看过我的文章，就会发现我是一个`MVC`主导开发的人。这是因为开发的项目总是算不上大项目，在合理的代码职责分工后项目能保持良好的状态，就没有使用到其他架构开发过项目（如果你的状态跟笔者差不多，就算不适用其他架构模式，你也应该自己学习）

![1](<https://allluckly.cn/images/blog/tuogao/tougao68/1.jpg>)

OK，简短来说，在很早之前我就有写这么一篇文章的想法，大致是在当初面试很多iOS开发者的时候这样的对话萌生的念头，下面的对话是经过笔者总结的，切勿对号入座：

> Q: 你在项目中使用了MVVM的架构结构，能说说为什么采用的是这种结构吗？

> A: 这是因为我们的项目在开发中控制器的代码越来越多，超过了一千行，然后觉得这样控制器的职责太多，就采用一个个ViewModel把这些职责分离出来

> Q: 能说说你们控制器的职责吗？或者有源码可以参考一下吗？

> 面试者拿出电脑展示源码

最后的结果就是，笔者不认为面试者需要使用到`MVVM`来改进他们的架构，这里当然是见仁见智了。由于对方代码职责的不合理分工导致了`View`和`Model`层几乎没有业务逻辑，从而导致了控制器的失衡，变得笨重。在这种情况下即便他使用了`ViewModel`将控制器的代码分离了出来，充其量只是`将垃圾挪到另一个地方罢了`。我在[MVC架构杂谈](<http://www.jianshu.com/p/4847c9a1e19b>)中提到过自身对`MVC`三个模块的职责认识，当你想将`MVC`改进成`MVX`的其他结构时，应当先思考自己的代码职责是不是已经均衡了。

## 码农小明的项目

在开始之前，还是强烈推荐推荐`《重构-改善既有代码的设计》`这本书，一本好书或者好文章应该让你每次观赏时都能产生不同的感觉。

正常来说，造成你代码笨重的最大凶手是重复的代码，例如曾经笔者看过这样一张界面图以及逻辑代码：

![1](<https://allluckly.cn/images/blog/tuogao/tougao68/2.jpg>)

```
@interface XXXViewController

@property (weak, nonatomic) IBOutlet UIButton * rule1;
@property (weak, nonatomic) IBOutlet UIButton * rule2;
@property (weak, nonatomic) IBOutlet UIButton * rule3;
@property (weak, nonatomic) IBOutlet UIButton * rule4;

@end

@implementation XXXViewController

- (IBAction)actionToClickRule1: (id)sender {
    [_rule1 setSelected: YES];
    [_rule2 setSelected: NO];
    [_rule3 setSelected: NO];
    [_rule4 setSelected: NO];
}

- (IBAction)actionToClickRule2: (id)sender {
    [_rule1 setSelected: NO];
    [_rule2 setSelected: YES];
    [_rule3 setSelected: NO];
    [_rule4 setSelected: NO];
}

- (IBAction)actionToClickRule1: (id)sender {
    [_rule1 setSelected: NO];
    [_rule2 setSelected: NO];
    [_rule3 setSelected: YES];
    [_rule4 setSelected: NO];
}

- (IBAction)actionToClickRule1: (id)sender {
    [_rule1 setSelected: NO];
    [_rule2 setSelected: NO];
    [_rule3 setSelected: NO];
    [_rule4 setSelected: YES];
}

@end
```

别急着嘲笑这样的代码，曾经的我们也写过类似的代码。这就是最直接粗浅的重复代码，所有的重复代码都和上面存在一样的毛病：亢长、无意义、占用了大量的空间。实际上，这些重复的代码总是分散在多个类当中，积少成多让我们的代码变得笨重。因此，在讨论你的项目是否需要改进架构之前，先弄清楚你是否需要消除这些垃圾。

举个例子，小明开发的一款面向B端的应用中允许商户添加优惠活动，包括开始日期和结束日期：

```
@interface Promotion: NSObject

+ (instancetype)currentPromotion;

@property (readonly, nonatomic) CGFloat discount;
@property (readonly, nonatomic) NSDate * start;
@property (readonly, nonatomic) NSDate * end;

@end
```

由于商户同一时间只会存在一个优惠活动，小明把活动写成了单例，然后其他模块通过获取活动单例来计算折后价格：

```
//  module A
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
CGFloat discountAmount = _order.amount;
if ([now timeIntervalSinceDate: promotion.start] > 0 && [now timeIntervalSinceDate: promotion.end] < 0) {
    discountAmount *= promotion.discount;
}

//  module B
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
if ([now timeIntervalSinceDate: promotion.start] > 0 && [now timeIntervalSinceDate: promotion.end] < 0) {
    [_cycleDisplayView display: @"全场限时%g折", promotion.discount*10];
}

//  module C
...
```

小明在开发完成后优化代码时发现了多个模块存在这样的重复代码，于是他写了一个`NSDate`的扩展来简化了这段代码，顺便还添加了一个安全监测：

```
@implementation NSDate (convenience)

- (BOOL)betweenFront: (NSDate *)front andBehind: (NSDate *)behind {
    if (!front || !behind) { return NO; }
    return ([self timeIntervalSinceDate: front] > 0 && [self timeIntervalSinceDate: behind] < 0);
}

@end

//  module A
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
CGFloat discountAmount = _order.amount;
if ([now betweenFront: promotion.start andBehind: promotion.end]) {
    discountAmount *= promotion.discount;
}

//  module B
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
if ([now betweenFront: promotion.start andBehind: promotion.end]) {
    [_cycleDisplayView display: @"全场限时%g折", promotion.discount*10];
}
```

过了一段时间，产品找到小明说：小明啊，商户反映说只有一个优惠活动是不够的，他们需要存在多个不同的活动。小明一想，那么就取消`Promotion`的单例属性，增加一个管理单例：

```
@interface PromotionManager: NSObject

@property (readonly, nonatomic) NSArray<Promotion *> * promotions

+ (instancetype)sharedManager;
- (void)requestPromotionsWithComplete: (void(^)(PromotionManager * manager))complete;

@end

//  module A
- (void)viewDidLoad {
    PromotionManager * manager = [PromotionManager sharedManager];
    if (manager.promotions) {
        [manager requestPromotionsWithComplete: ^(PromotionManager * manager) {
            _promotions = manager.promotions;
            [self calculateOrder];
        }
    } else {
        _promotions = manager.promotions;
        [self calculateOrder];
    }
}

- (void)calculateOrder {
    CGFloat orderAmount = _order.amount;
    for (Promotion * promotion in _promotions) {
        if ([[NSDate date] betweenFront: promotion.start andBehind: promotion.end]) {
            orderAmount *= promotion.discount;
        }
    }
}
```

随着日子一天天过去，产品提出的需求也越来越多。有一天，产品说应该让商户可以自由开关优惠活动，于是`Promotion`多了一个`isActived`是否激活的属性。其他模块的判断除了判断时间还多了判断是否启动了活动。再后来，还添加了一个`synchronize`属性判断是否可以与其他活动同时计算判断。最近产品告诉小明活动现在不仅局限于折扣，还新增了固定优惠，以及满额优惠，于是代码变成了下面这样:

```
@interface Promotion: NSObject

@property (assign, nonatomic) BOOL isActived;
@property (assign, nonatomic) BOOL synchronize;
@property (assign, nonatomic) CGFloat discount;
@property (assign, nonatomic) CGFloat discountCondition;
@property (assign, nonatomic) DiscountType discountType;
@property (assign, nonatomic) PromotionType promotionType;

@property (readonly, nonatomic) NSDate * start;
@property (readonly, nonatomic) NSDate * end;

@end

//  module A
- (void)viewDidLoad {
    PromotionManager * manager = [PromotionManager sharedManager];
    if (manager.promotions) {
        [manager requestPromotionsWithComplete: ^(PromotionManager * manager) {
            _promotions = manager.promotions;
            [self calculateOrder];
        }
    } else {
        _promotions = manager.promotions;
        [self calculateOrder];
    }
}

- (void)calculateOrder {
    CGFloat orderAmount = _order.amount;
    NSMutableArray * fullPromotions = @[].mutableCopy;
    NSMutableArray * discountPromotions = @[].mutableCopy;
    for (Promotion p in _promotions) {
        if (p.isActived && [[NSDate date] betweenFront: p.start andBehind: p.end]) {
            if (p.promotionType == PromotionTypeFullPromotion) {
                [fullPromotions addObject: p];
            } else if (p.promotionType == PromotionTypeDiscount) {
                [discountPromotions addObject: p];
            }
        }
    }

    Promotion * syncPromotion = nil;
    Promotion * singlePromotion = nil;
    for (Promotion * p in fullPromotions) {
        if (p.synchronize) {
            if (p.discountCondition != 0) {
                if (p.discountCondition > syncPromotion.discountCondition) {
                    syncPromotion = p;
                }
            } else {
                if (p.discount > syncPromotion.discount) {
                    syncPromotion = p;
                }
            }
        } else {
            if (p.discountCondition != 0) {
                if (p.discountCondition > singlePromotion.discountCondition) {
                    singlePromotion = p;
                }
            } else {
                if (p.discount > singlePromotion.discount) {
                    singlePromotion = p;
                }
            }
        }
    }
    //  find discount promotions
    ......
}
```

这时候模块获取优惠活动信息的代价已经变得十分的昂贵，一堆亢长的代码，重复度高。这时候小明的同事对他说，我们改进一下架构吧，通过`ViewModel`把这部分的代码从控制器分离出去。其实这时候`ViewModel`的做法跟上面小明直接扩展`NSDate`的目的是一样的，在这个时候`View`和`Model`几乎无作为，基本所有逻辑都在控制器中不断地撑胖它。小明认真思考，完完全全将代码阅览后，告诉同事现在最大的原因在于代码职责混乱，并不能很好的分离到`VC`的模块中，解决的方式应该是从逻辑分工下手。

首先，小明发现`Promotion`本身除了存储活动信息，没有进行任何的逻辑操作。而控制器中判断活动是否有效以及折扣金额计算的业务理可以由`Promotion`来完成：

```
@interface Promotion: NSObject

- (BOOL)isEffective;
- (BOOL)isWorking;
- (CGFloat)discountAmount: (CGFloat)amount;

@end

@implementation Promotion

- (BOOL)isEffective {
    return [[NSDate date] betweenFront: _start andBehind: _end];
}

- (BOOL)isWorking {
    return ( [self isEffective] && _isActived );
}

- (CGFloat)discountAmount: (CGFloat)amount {
    if ([self isWorking]) {
        if (_promotionType == PromotionTypeDiscount) {
            return [self calculateDiscount: amount];
        } else {
            if (amount < _discountCondition) { return amount; }
            return [self calculateDiscount: amount];
        }
    }
    return amount;
}

#pragma mark - Private
- (CGFloat)calculateDiscount: (CGFloat)amount {
    if (_discountType == DiscountTypeCoupon) {
        return amount - _discount;
    } else {
        return amount * _discount;
    }
}

@end
```

除此之外，小明发现先前封装的活动管理类`PromotionManager`本身涉及了网络请求和数据管理两个业务，因此需要将其中一个业务分离出来。于是网络请求封装成`PromotionRequest`，另一方面原有的数据管理只有获取数据的功能，因此增加增删改以及对活动进行初步筛选的功能：

```
#pragma mark -  PromotionManager.h
@class PromotionManager;
typeof void(^PromotionRequestComplete)(PromotionManager * manager);

@interface PromotionRequest: NSObject

+ (void)requestPromotionsWithComplete: (PromotionRequestComplete)complete;
+ (void)insertPromotion: (Promotion *)promotion withComplete: (PromotionRequestComplete)complete;
+ (void)updatePromotion: (Promotion *)promotion withComplete: (PromotionRequestComplete)complete;
+ (void)deletePromotion: (Promotion *)promotion withComplete: (PromotionRequestComplete)complete;

@end


@interface PromotionManager: NSObject

+ (instancetype)sharedManager;

- (NSArray<Promotion *> *)workingPromotions;
- (NSArray<Promotion *> *)effectivePromotions;
- (NSArray<Promotion *> *)fullPromotions;
- (NSArray<Promotion *> *)discountPromotions;

- (void)insertPromotion: (Promotion *)promotion;
- (void)updatePromotion: (Promotion *)promotion;
- (void)deletePromotion: (Promotion *)promotion;

@end



#pragma mark -  PromotionManager.m
@interface PromotionManager ()

@property (nonatomic, strong) NSArray<Promotion *> * promotions;

@end

@implementation PromotionManager

+ (instancetype)sharedManager { ... }

- (NSArray<Promotion *> *)fullPromotions {
    return [self filterPromotionsWithType: PromotionTypeFullPromote];
}

- (NSArray<Promotion *> *)discountPromotions {
    return [self filterPromotionsWithType: PromotionDiscountPromote];
}

- (NSArray<Promotion *> *)workingPromotions {
    return _promotions.filter(^BOOL(Promotion * p) {
        return (p.isWorking);
    });
}

- (NSArray<Promotion *> *)effectivePromotions {
    return _promotions.filter(^BOOL(Promotion * p) {
        return (p.isEffective);
    });
}

- (NSArray<Promotion *> *)filterPromotionsWithType: (PromotionType)type {
    return [self workingPromotions].filter(^BOOL(Promotion * p) {
        return (p.promotionType == type);
    });
}

- (void)insertPromotion: (Promotion *)promotion { 
    if ([_promotions containsObject: promotion]) {
        [PromotionRequest updatePromotion: promotion withComplete: nil];
    } else {
        [PromotionRequest insertPromotion: promotion withComplete: nil];
    }
 }

- (void)updatePromotion: (Promotion *)promotion { 
    if ([_promotions containsObject: promotion]) {
        [PromotionRequest updatePromotion: promotion withComplete: nil];
    }
 }

- (void)deletePromotion: (Promotion *)promotion { 
    if ([_promotions containsObject: promotion]) {
        [PromotionRequest deletePromotion: promotion withComplete: nil];
    }
}

- (void)obtainPromotionsFromJSON: (id)JSON { ... }

@end
```

最后，小明发现其他模块在寻找最优惠活动的逻辑代码非常的多，另外由于存在满额优惠和普通优惠两种活动，进一步加大了代码量。因此小明新建了一个计算类`PromotionCalculator`用来完成查找最优活动和计算最优价格的接口：

```
@interface PromotionCalculator: NSObject

+ (CGFloat)calculateAmount: (CGFloat)amount;
+ (Promotion *)bestFullPromotion: (CGFloat)amount;
+ (Promotion *)bestDiscountPromotion: (CGFloat)amount;

@end

@implementation PromotionCalculator

+ (CGFloat)calculateAmount: (CGFloat)amount {
    Promotion * bestFullPromotion = [self bestFullPromotion: amount];
    Promotion * bestDiscountPromotion = [self bestDiscountPromotion: amount];
    if (bestFullPromotion.synchronize && bestDiscountPromotion.synchronize) {
        return [bestFullPromotion discountAmount: [bestDiscountPromotion discountAmount: amount]];
    } else {
        return MAX([bestDiscountPromotion discountAmount: amount], [bestFullPromotion discountAmount: amount]);
    }
}

+ (Promotion *)bestFullPromotion: (CGFloat)amount {
    PromotionManager * manager = [PromotionManager sharedManager];
    return [self bestPromotionInPromotions: [manager fullPromotions] amount: amount];
}

+ (Promotion *)bestDiscountPromotion: (CGFloat)amount {
    PromotionManager * manager = [PromotionManager sharedManager];
    return [self bestPromotionInPromotions: [manager discountPromotions] amount: amount];
}  

+ (Promotion *)bestPromotionInPromotions: (NSArray *)promotions amount: (CGFloat)amount {
    CGFloat discount = amount;
    Promotion * best = nil;
    for (Promotion * promotion in promotions) {
        CGFloat tmp = [promotion discountAmount: amount];
        if (tmp < discount) {
            discount = tmp;
            best = promotion;
        }
    }
    return best;
}

@end
```

当这些代码逻辑被小明分散到各处之后，小明惊讶的发现其他模块在进行计算时剩下几行代码而已：

```
- (void)viewDidLoad {
    [PromotionRequest requestPromotionsWithComplete: ^(PromotionManager * manager) {
        _discountAmount = [PromotionCalculator calculateAmount: _order.amount];
    }];
} 这时候代码职责的结构图，小明成功的均衡了不同组件之间的代码职责，避免了改变项目原架构带来的风险以及不必要的工作：
```

![1](<https://allluckly.cn/images/blog/tuogao/tougao68/3.jpg>)

## 尾语

这是第二篇讲`MVC`的文章，仍然要告诉大家的是`MVC`确确实实存在着缺陷，这个缺陷会在项目变得很大的时候暴露出来（笔者没有开发过大型项目的弱鸡），如果你的项目结构分层做的足够完善的话，那么该改进更换架构的时候就不要犹豫。但千万要记住，如果仅仅是因为重复了太多的无用代码，又或者是逻辑全部塞到控制器中，那么更换架构无非是将垃圾再次分散罢了。

---

好文推荐：[分分钟解决iOS开发中App启动动画广告的功能](<https://allluckly.cn/lblaunchimagead/LBLaunchImageAd>)

