前言
文章的標(biāo)題有點(diǎn)繞口,不過想了半天,想不到更好的標(biāo)題了求泰。本文的誕生有一部分功勞要?dú)w于iOS應(yīng)用現(xiàn)狀分析,標(biāo)題也是來源于原文中的“能把代碼職責(zé)均衡的劃分到不同的功能類里”计盒。如果你看過我的文章渴频,就會(huì)發(fā)現(xiàn)我是一個(gè)MVC
主導(dǎo)開發(fā)的人。這是因?yàn)殚_發(fā)的項(xiàng)目總是算不上大項(xiàng)目北启,在合理的代碼職責(zé)分工后項(xiàng)目能保持良好的狀態(tài)卜朗,就沒有使用到其他架構(gòu)開發(fā)過項(xiàng)目(如果你的狀態(tài)跟筆者差不多,就算不適用其他架構(gòu)模式咕村,你也應(yīng)該自己學(xué)習(xí))
OK场钉,簡短來說,在很早之前我就有寫這么一篇文章的想法懈涛,大致是在當(dāng)初面試很多iOS開發(fā)者的時(shí)候這樣的對(duì)話萌生的念頭逛万,下面的對(duì)話是經(jīng)過筆者總結(jié)的,切勿對(duì)號(hào)入座:
Q: 你在項(xiàng)目中使用了MVVM的架構(gòu)結(jié)構(gòu)批钠,能說說為什么采用的是這種結(jié)構(gòu)嗎宇植?
A: 這是因?yàn)槲覀兊捻?xiàng)目在開發(fā)中控制器的代碼越來越多,超過了一千行埋心,然后覺得這樣控制器的職責(zé)太多指郁,就采用一個(gè)個(gè)ViewModel把這些職責(zé)分離出來
Q: 能說說你們控制器的職責(zé)嗎?或者有源碼可以參考一下嗎拷呆?
面試者拿出電腦展示源碼
最后的結(jié)果就是闲坎,筆者不認(rèn)為面試者需要使用到MVVM
來改進(jìn)他們的架構(gòu),這里當(dāng)然是見仁見智了。由于對(duì)方代碼職責(zé)的不合理分工導(dǎo)致了View
和Model
層幾乎沒有業(yè)務(wù)邏輯箫柳,從而導(dǎo)致了控制器的失衡手形,變得笨重。在這種情況下即便他使用了ViewModel
將控制器的代碼分離了出來悯恍,充其量只是將垃圾挪到另一個(gè)地方罷了
库糠。我在MVC架構(gòu)雜談中提到過自身對(duì)MVC
三個(gè)模塊的職責(zé)認(rèn)識(shí),當(dāng)你想將MVC
改進(jìn)成MVX
的其他結(jié)構(gòu)時(shí)涮毫,應(yīng)當(dāng)先思考自己的代碼職責(zé)是不是已經(jīng)均衡了瞬欧。
碼農(nóng)小明的項(xiàng)目
在開始之前,還是強(qiáng)烈推薦推薦《重構(gòu)-改善既有代碼的設(shè)計(jì)》
這本書罢防,一本好書或者好文章應(yīng)該讓你每次觀賞時(shí)都能產(chǎn)生不同的感覺艘虎。
正常來說,造成你代碼笨重的最大兇手是重復(fù)的代碼咒吐,例如曾經(jīng)筆者看過這樣一張界面圖以及邏輯代碼:
@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
別急著嘲笑這樣的代碼野建,曾經(jīng)的我們也寫過類似的代碼。這就是最直接粗淺的重復(fù)代碼恬叹,所有的重復(fù)代碼都和上面存在一樣的毛埠蛏:亢長、無意義绽昼、占用了大量的空間唯鸭。實(shí)際上,這些重復(fù)的代碼總是分散在多個(gè)類當(dāng)中硅确,積少成多讓我們的代碼變得笨重目溉。因此,在討論你的項(xiàng)目是否需要改進(jìn)架構(gòu)之前菱农,先弄清楚你是否需要消除這些垃圾缭付。
舉個(gè)例子,小明開發(fā)的一款面向B端的應(yīng)用中允許商戶添加優(yōu)惠活動(dòng)大莫,包括開始日期和結(jié)束日期:
@interface Promotion: NSObject
+ (instancetype)currentPromotion;
@property (readonly, nonatomic) CGFloat discount;
@property (readonly, nonatomic) NSDate * start;
@property (readonly, nonatomic) NSDate * end;
@end
由于商戶同一時(shí)間只會(huì)存在一個(gè)優(yōu)惠活動(dòng)蛉腌,小明把活動(dòng)寫成了單例,然后其他模塊通過獲取活動(dòng)單例來計(jì)算折后價(jià)格:
// 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: @"全場(chǎng)限時(shí)%g折", promotion.discount*10];
}
// module C
...
小明在開發(fā)完成后優(yōu)化代碼時(shí)發(fā)現(xiàn)了多個(gè)模塊存在這樣的重復(fù)代碼只厘,于是他寫了一個(gè)NSDate
的擴(kuò)展來簡化了這段代碼烙丛,順便還添加了一個(gè)安全監(jiān)測(cè):
@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: @"全場(chǎng)限時(shí)%g折", promotion.discount*10];
}
過了一段時(shí)間,產(chǎn)品找到小明說:小明啊羔味,商戶反映說只有一個(gè)優(yōu)惠活動(dòng)是不夠的河咽,他們需要存在多個(gè)不同的活動(dòng)。小明一想赋元,那么就取消Promotion
的單例屬性忘蟹,增加一個(gè)管理單例:
@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;
}
}
}
隨著日子一天天過去飒房,產(chǎn)品提出的需求也越來越多。有一天媚值,產(chǎn)品說應(yīng)該讓商戶可以自由開關(guān)優(yōu)惠活動(dòng)狠毯,于是Promotion
多了一個(gè)isActived
是否激活的屬性。其他模塊的判斷除了判斷時(shí)間還多了判斷是否啟動(dòng)了活動(dòng)褥芒。再后來嚼松,還添加了一個(gè)synchronize
屬性判斷是否可以與其他活動(dòng)同時(shí)計(jì)算判斷。最近產(chǎn)品告訴小明活動(dòng)現(xiàn)在不僅局限于折扣锰扶,還新增了固定優(yōu)惠献酗,以及滿額優(yōu)惠,于是代碼變成了下面這樣:
@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
......
}
這時(shí)候模塊獲取優(yōu)惠活動(dòng)信息的代價(jià)已經(jīng)變得十分的昂貴坷牛,一堆亢長的代碼罕偎,重復(fù)度高。這時(shí)候小明的同事對(duì)他說京闰,我們改進(jìn)一下架構(gòu)吧颜及,通過ViewModel
把這部分的代碼從控制器分離出去。其實(shí)這時(shí)候ViewModel
的做法跟上面小明直接擴(kuò)展NSDate
的目的是一樣的忙干,在這個(gè)時(shí)候View
和Model
幾乎無作為器予,基本所有邏輯都在控制器中不斷地?fù)闻炙嗽濉P∶髡J(rèn)真思考捐迫,完完全全將代碼閱覽后,告訴同事現(xiàn)在最大的原因在于代碼職責(zé)混亂爱葵,并不能很好的分離到VC
的模塊中施戴,解決的方式應(yīng)該是從邏輯分工下手。
首先萌丈,小明發(fā)現(xiàn)Promotion
本身除了存儲(chǔ)活動(dòng)信息赞哗,沒有進(jìn)行任何的邏輯操作。而控制器中判斷活動(dòng)是否有效以及折扣金額計(jì)算的業(yè)務(wù)理可以由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
除此之外辆雾,小明發(fā)現(xiàn)先前封裝的活動(dòng)管理類PromotionManager
本身涉及了網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)管理兩個(gè)業(yè)務(wù)肪笋,因此需要將其中一個(gè)業(yè)務(wù)分離出來。于是網(wǎng)絡(luò)請(qǐng)求封裝成PromotionRequest
度迂,另一方面原有的數(shù)據(jù)管理只有獲取數(shù)據(jù)的功能藤乙,因此增加增刪改以及對(duì)活動(dòng)進(jìn)行初步篩選的功能:
#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
最后,小明發(fā)現(xiàn)其他模塊在尋找最優(yōu)惠活動(dòng)的邏輯代碼非常的多惭墓,另外由于存在滿額優(yōu)惠和普通優(yōu)惠兩種活動(dòng)坛梁,進(jìn)一步加大了代碼量。因此小明新建了一個(gè)計(jì)算類PromotionCalculator
用來完成查找最優(yōu)活動(dòng)和計(jì)算最優(yōu)價(jià)格的接口:
@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
當(dāng)這些代碼邏輯被小明分散到各處之后腊凶,小明驚訝的發(fā)現(xiàn)其他模塊在進(jìn)行計(jì)算時(shí)剩下幾行代碼而已:
- (void)viewDidLoad {
[PromotionRequest requestPromotionsWithComplete: ^(PromotionManager * manager) {
_discountAmount = [PromotionCalculator calculateAmount: _order.amount];
}];
}
這時(shí)候代碼職責(zé)的結(jié)構(gòu)圖划咐,小明成功的均衡了不同組件之間的代碼職責(zé)拴念,避免了改變項(xiàng)目原架構(gòu)帶來的風(fēng)險(xiǎn)以及不必要的工作:
尾語
這是第二篇講MVC
的文章,仍然要告訴大家的是MVC
確確實(shí)實(shí)存在著缺陷褐缠,這個(gè)缺陷會(huì)在項(xiàng)目變得很大的時(shí)候暴露出來(筆者沒有開發(fā)過大型項(xiàng)目的弱雞)政鼠,如果你的項(xiàng)目結(jié)構(gòu)分層做的足夠完善的話,那么該改進(jìn)更換架構(gòu)的時(shí)候就不要猶豫队魏。但千萬要記住缔俄,如果僅僅是因?yàn)橹貜?fù)了太多的無用代碼,又或者是邏輯全部塞到控制器中器躏,那么更換架構(gòu)無非是將垃圾再次分散罷了俐载。
關(guān)注iOS開發(fā)獲得筆者更新動(dòng)態(tài)
轉(zhuǎn)載請(qǐng)注明地址以及作者