前言
文章的標題有點繞口,不過想了半天,想不到更好的標題了。本文的誕生有一部分功勞要歸于iOS應用現狀分析,標題也是來源于原文中的“能把代碼職責均衡的劃分到不同的功能類里”。如果你看過我的文章,就會發現我是一個MVC
主導開發的人。這是因為開發的項目總是算不上大項目,在合理的代碼職責分工后項目能保持良好的狀態,就沒有使用到其他架構開發過項目(如果你的狀態跟筆者差不多,就算不適用其他架構模式,你也應該自己學習)
OK,簡短來說,在很早之前我就有寫這么一篇文章的想法,大致是在當初面試很多iOS開發者的時候這樣的對話萌生的念頭,下面的對話是經過筆者總結的,切勿對號入座:
Q: 你在項目中使用了MVVM的架構結構,能說說為什么采用的是這種結構嗎?
A: 這是因為我們的項目在開發中控制器的代碼越來越多,超過了一千行,然后覺得這樣控制器的職責太多,就采用一個個ViewModel把這些職責分離出來
Q: 能說說你們控制器的職責嗎?或者有源碼可以參考一下嗎?
面試者拿出電腦展示源碼
最后的結果就是,筆者不認為面試者需要使用到MVVM
來改進他們的架構,這里當然是見仁見智了。由于對方代碼職責的不合理分工導致了View
和Model
層幾乎沒有業務邏輯,從而導致了控制器的失衡,變得笨重。在這種情況下即便他使用了ViewModel
將控制器的代碼分離了出來,充其量只是將垃圾挪到另一個地方罷了
。我在MVC架構雜談中提到過自身對MVC
三個模塊的職責認識,當你想將MVC
改進成MVX
的其他結構時,應當先思考自己的代碼職責是不是已經均衡了。
碼農小明的項目
在開始之前,還是強烈推薦推薦《重構-改善既有代碼的設計》
這本書,一本好書或者好文章應該讓你每次觀賞時都能產生不同的感覺。
正常來說,造成你代碼笨重的最大兇手是重復的代碼,例如曾經筆者看過這樣一張界面圖以及邏輯代碼:
@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];
}];
}
這時候代碼職責的結構圖,小明成功的均衡了不同組件之間的代碼職責,避免了改變項目原架構帶來的風險以及不必要的工作:
尾語
這是第二篇講MVC
的文章,仍然要告訴大家的是MVC
確確實實存在著缺陷,這個缺陷會在項目變得很大的時候暴露出來(筆者沒有開發過大型項目的弱雞),如果你的項目結構分層做的足夠完善的話,那么該改進更換架構的時候就不要猶豫。但千萬要記住,如果僅僅是因為重復了太多的無用代碼,又或者是邏輯全部塞到控制器中,那么更換架構無非是將垃圾再次分散罷了。
關注iOS開發獲得筆者更新動態
轉載請注明地址以及作者