iOS 开发 – 均衡代码职责
前言
文章的标题有点绕口,不过想了半天,想不到更好的标题了。本文的诞生有一部分功劳要归于iOS应用现状分析,标题也是来源于原文中的“能把代码职责均衡的划分到不同的功能类里”。如果你看过我的文章,就会发现我是一个MVC
主导开发的人。这是因为开发的项目总是算不上大项目,在合理的代码职责分工后项目能保持良好的状态,就没有使用到其他架构开发过项目(如果你的状态跟笔者差不多,就算不适用其他架构模式,你也应该自己学习)
OK,简短来说,在很早之前我就有写这么一篇文章的想法,大致是在当初面试很多iOS开发者的时候这样的对话萌生的念头,下面的对话是经过笔者总结的,切勿对号入座:
Q: 你在项目中使用了MVVM的架构结构,能说说为什么采用的是这种结构吗?
A: 这是因为我们的项目在开发中控制器的代码越来越多,超过了一千行,然后觉得这样控制器的职责太多,就采用一个个ViewModel把这些职责分离出来
Q: 能说说你们控制器的职责吗?或者有源码可以参考一下吗?
面试者拿出电脑展示源码
最后的结果就是,笔者不认为面试者需要使用到MVVM
来改进他们的架构,这里当然是见仁见智了。由于对方代码职责的不合理分工导致了View
和Model
层几乎没有业务逻辑,从而导致了控制器的失衡,变得笨重。在这种情况下即便他使用了ViewModel
将控制器的代码分离了出来,充其量只是将垃圾挪到另一个地方罢了
。我在MVC架构杂谈中提到过自身对MVC
三个模块的职责认识,当你想将MVC
改进成MVX
的其他结构时,应当先思考自己的代码职责是不是已经均衡了。
码农小明的项目
在开始之前,还是强烈推荐推荐《重构-改善既有代码的设计》
这本书,一本好书或者好文章应该让你每次观赏时都能产生不同的感觉。
正常来说,造成你代码笨重的最大凶手是重复的代码,例如曾经笔者看过这样一张界面图以及逻辑代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@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端的应用中允许商户添加优惠活动,包括开始日期和结束日期:
1
2
3
4
5
6
7
8
9
|
@interface Promotion: NSObject
+ (instancetype)currentPromotion;
@property (readonly, nonatomic) CGFloat discount;
@property (readonly, nonatomic) NSDate * start;
@property (readonly, nonatomic) NSDate * end;
@end
|
由于商户同一时间只会存在一个优惠活动,小明把活动写成了单例,然后其他模块通过获取活动单例来计算折后价格:
1
2
3
4
5
|
// 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 && [now timeIntervalSinceDate: promotion.end]
|
小明在开发完成后优化代码时发现了多个模块存在这样的重复代码,于是他写了一个NSDate
的扩展来简化了这段代码,顺便还添加了一个安全监测:
Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@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
的单例属性,增加一个管理单例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@interface PromotionManager: NSObject
@property (readonly, nonatomic) NSArray * 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
属性判断是否可以与其他活动同时计算判断。最近产品告诉小明活动现在不仅局限于折扣,还新增了固定优惠,以及满额优惠,于是代码变成了下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
@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
来完成:
Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@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
,另一方面原有的数据管理只有获取数据的功能,因此增加增删改以及对活动进行初步筛选的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
#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 *)workingPromotions;
- (NSArray *)effectivePromotions;
- (NSArray *)fullPromotions;
- (NSArray *)discountPromotions;
- (void)insertPromotion: (Promotion *)promotion;
- (void)updatePromotion: (Promotion *)promotion;
- (void)deletePromotion: (Promotion *)promotion;
@end
#pragma mark - PromotionManager.m
@interface PromotionManager ()
@property (nonatomic, strong) NSArray * promotions;
@end
@implementation PromotionManager
+ (instancetype)sharedManager { ... }
- (NSArray *)fullPromotions {
return [self filterPromotionsWithType: PromotionTypeFullPromote];
}
- (NSArray *)discountPromotions {
return [self filterPromotionsWithType: PromotionDiscountPromote];
}
- (NSArray *)workingPromotions {
return _promotions.filter(^BOOL(Promotion * p) {
return (p.isWorking);
});
}
- (NSArray *)effectivePromotions {
return _promotions.filter(^BOOL(Promotion * p) {
return (p.isEffective);
});
}
- (NSArray *)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
用来完成查找最优活动和计算最优价格的接口:
Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
@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
|
当这些代码逻辑被小明分散到各处之后,小明惊讶的发现其他模块在进行计算时剩下几行代码而已:
1
2
3
4
5
|
- (void)viewDidLoad {
[PromotionRequest requestPromotionsWithComplete: ^(PromotionManager * manager) {
_discountAmount = [PromotionCalculator calculateAmount: _order.amount];
}];
}
|
这时候代码职责的结构图,小明成功的均衡了不同组件之间的代码职责,避免了改变项目原架构带来的风险以及不必要的工作:
尾语
这是第二篇讲MVC
的文章,仍然要告诉大家的是MVC
确确实实存在着缺陷,这个缺陷会在项目变得很大的时候暴露出来(笔者没有开发过大型项目的弱鸡),如果你的项目结构分层做的足够完善的话,那么该改进更换架构的时候就不要犹豫。但千万要记住,如果仅仅是因为重复了太多的无用代码,又或者是逻辑全部塞到控制器中,那么更换架构无非是将垃圾再次分散罢了。
关注iOS开发获得笔者更新动态
转载请注明地址以及作者
转载:http://ios.jobbole.com/90045/
iOS 开发 – 均衡代码职责的更多相关文章
- iOS开发通过代码方式使用AutoLayout (NSLayoutConstraint + Masonry)
iOS开发通过代码方式使用AutoLayout (NSLayoutConstraint + Masonry) 随着iPhone6/6+设备的上市,如何让手头上的APP适配多种机型多种屏幕尺寸变得尤为迫 ...
- iOS开发 纯代码创建UICollectionView
转:http://jingyan.baidu.com/article/eb9f7b6d8a81a5869364e8a6.html iOS开发 纯代码创建UICollectionView 习惯了使用xi ...
- 设计模式之单一职责原则(iOS开发,代码用Objective-C展示)
单一职责原则:就一个类而言,应该只有一个引起它变化的原因. 在iOS开发中,我们会很自然的给一个类添加各种各样的功能,比如随便写一个简单的应用程序,一般都会生成一个viewController类,于是 ...
- iOS开发:代码通用性以及其规范 第二篇(猜想iOS中实现TableView内部设计思路(附代码),以类似的思想实现一个通用的进度条)
在iOS开发中,经常是要用到UITableView的,我曾经思考过这样一个问题,为什么任何种类的model放到TableView和所需的cell里面,都可以正常显示?而我自己写的很多view却只是能放 ...
- iOS开发:代码通用性以及其规范 第一篇(附带,自定义UITextView\进度条\双表显示\瀑布流 代码设计思路)
在iOS团队开发中,我见过一些人的代码,也修改过他们的代码.有的人的代码写的非常之规范.通用,几乎不用交流,就可以知道如何修改以及在它基础上扩展延生.有的人的代码写的很垃圾,一眼看过去,简直会怀疑自己 ...
- IOS开发之----代码块的使用(二)
iOS4引入了一个新特性,支持代码块的使用,这将从根本上改变你的编程方式.代码块是对C语言的一个扩展,因此在Objective-C中完全支持.如果你学过Ruby,Python或Lisp编程语言,那么你 ...
- IOS开发通过代码方式使用AutoLayout (NSLayoutConstraint + Masonry) 转载
http://blog.csdn.net/he_jiabin/article/details/48677911 随着iPhone6/6+设备的上市,如何让手头上的APP适配多种机型多种屏幕尺寸变得尤为 ...
- iOS开发常用代码块(第二弹)
GCD定时器 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ); dispat ...
- iOS开发常用代码块
遍历可变数组的同时删除数组元素 NSMutableArray *copyArray = [NSMutableArray arrayWithArray:array]; NSString *str1 = ...
随机推荐
- Java_一些特殊的关键字详(?)解
1. native. 在看JDK源码时,想知道Math包里比如cos, sin等函数是如何实现的(猜想的是用他们在0点的泰勒展开式近似),但是打开之后却发现只有一个函数声明,并且有关键字native, ...
- Python入门-行和缩进
学习Python与其他语言最大的区别就是,Python的代码块不使用大括号({})来控制类,函数以及其他逻辑判断.python最具特色的就是用缩进来写模块. 缩进的空白数量是可变的,但是所有代码块语句 ...
- python_way day18 html-day4, Django路由,(正则匹配页码,包含自开发分页功能), 模板, Model(jDango-ORM) : SQLite,数据库时间字段插入的方法
python_way day18 html-day4 1.Django-路由系统 - 自开发分页功能 2.模板语言:之母板的使用 3.SQLite:model(jDango-ORM) 数据库时间字 ...
- js中的 !!
就是这样:!!variable.哈哈,其实我也是在代码里面看见别人这样用,当时很好奇,所以就搜了一下,哈哈.还真的有很多相关的好文啊.作者是这样说的, 一般用来将后面的表达式转换为布尔型的数据 是不是 ...
- mysql中替换行首字符
替换行首字符,而不替换字段中其它地方指定字符. UPDATE table SET open_time = CONCAT('W', open_time) WHERE open_time REGEXP ' ...
- sessionKey
许多人都知道NETSCAPE公司是Internet商业中领先技术的提供者,该公司提供了一种基于RSA和保密密钥的应用于因特网的技术,被称为安全插座层(Secure Sockets Layer,SSL) ...
- c++ 复习练习
复习c++的时候,发现一篇 如何通过c++ primer学习c++的好文,并列出了一些建议的练习题目. 链接,http://blog.csdn.net/solstice/article/details ...
- mysql与mysqld
mysql是客户机/服务器的结构. mysql是客户端行工具,连接mysqld服务,执行sql命令,可认为客户端sdk mysqld 启动mysql数据库服务. 脚本启动mysql服务的命令是 net ...
- Oracle一列的多行数据拼成一行显示字符
Oracle一列的多行数据拼成一行显示字符 oracle 提供了两个函数WMSYS.WM_CONCAT 和 ListAgg函数. www.2cto.com 先介绍:WMSYS.WM_CO ...
- Python学习笔记5-元组
元组是用圆括号括起来的,其中的元素之间用逗号隔开 >>> t = 123,'abc',["come","here"] >>> ...