前言
文章的標題有點繞口,不過想了半天,想不到更好的標題了。本文的誕生有一部分功勞要歸於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
的擴充套件來簡化了這段程式碼,順便還新增了一個安全監測:
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
來完成:
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
用來完成查詢最優活動和計算最優價格的介面:
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開發獲得筆者更新動態
轉載請註明地址以及作者