開場
今天, 我們將從一個小功能開始, 先去不假思索的實現它
- Product Repository: Filtering Operation
Code start
有一個產品庫, 我們要對它做過濾操作.
第一個需求並不複雜.
- 需求1:在倉庫中查詢所有顏色為紅色的產品
First Attempt: Hard Code
我們先用最簡單的方式去實現它, 硬編碼
1 2 3 4 5 6 7 8 9 10 |
- (NSArray *)findAllRedProducts:(NSArray *)products { NSMutableArray *list = [@[] mutableCopy]; for (Product *product in products) { if (product.color == RED) { [list addObject:product]; } } return list; } |
如果這個世界是永恆靜止的,這樣的實現無可厚非,但世界往往並非如此。
緊接著,第二個需求來了
- 需求2:在倉庫中查詢所有顏色為綠色的產品
Second Attempt: Parameterizing
Copy-Paste是大部分程式設計師最容易犯的毛病,為此引入了大量的重複程式碼。
1 2 3 4 5 6 7 8 9 10 |
- (NSArray *)findAllGreenProducts:(NSArray *)products { NSMutableArray *list = [@[] mutableCopy]; for (Product *product in products) { if (product.color == GREEN) { [list addObject:product]; } } return list; } |
為了消滅硬編碼,得到可重用的程式碼,可以引入簡單的引數化設計。
1 2 3 4 5 6 7 8 9 10 |
- (NSArray *)findProducts:(NSArray *)products byColor:(ProductColor)color { NSMutableArray *list = [@[] mutableCopy]; for (Product *product in products) { if (product.color == color) { [list addObject:product]; } } return list; } |
終於可以放心了, 這個時候我們的產品經理怎麼可能讓你舒服呢,需求3又來了
- 需求3:查詢所有重量小於10的所有產品
Third Attempt: Parameterizing with Every Attribute You Can Think Of
大部分程式設計師依然會使用Copy-Paste解決這個問題,拒絕Copy-Paste的陋習,最具實效的一個反饋就是讓這個快捷鍵失效,從而在每次嘗試Copy-Paste時提醒自己做更好的設計。
1 2 3 4 5 6 7 8 9 10 |
- (NSArray *)findProducts:(NSArray *)products byWeith:(float)weight { NSMutableArray *list = [@[] mutableCopy]; for (Product *product in products) { if (product.weight < weight) { [list addObject:product]; } } return list; } |
為了消除兩者重複的程式碼,通過簡單的引數化往往不能完美解決這類問題,相反地會引入過度的複雜度和偶發成本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (NSArray *)findProducts:(NSArray *)products byColor:(ProductColor)color byWeith:(float)weight type:(int)type { NSMutableArray *list = [@[] mutableCopy]; for (Product *product in products) { if ((type == 1) && product.color == color) { [list addObject:product]; continue; } else if ((type == 2) && (product.weight < weight)) { [list addObject:product]; continue; } } return list; } |
日常工作中,這樣的實現手法非常普遍,函式的引數列表隨著需求增加不斷增加,函式邏輯承擔的職責越來越多,邏輯也變得越來越難以控制。
- 通過引數配置應對變化的設計往往都是失敗的設計
- 易於導致複雜的邏輯控制,引發額外的偶發複雜度
Forth Attempt: Abstracting over Criteria
為此需要抽象,使其遍歷的演算法與查詢的標準能夠獨立地變化,互不影響。
1 2 3 |
@interface ProductSpec : NSObject - (BOOL)satisfy:(Product *)product; @end |
此刻filter的演算法邏輯得到封閉,當然函式名需要重新命名,使其演算法實現更加具有普遍性。
1 2 3 4 5 6 7 8 9 10 |
- (NSArray *)findProducts:(NSArray *)products bySpec:(ProductSpec *)spec { NSMutableArray *list = [@[] mutableCopy]; for (Product *product in products) { if ([spec satisfy:product]) { [list addObject:product]; } } return list; } |
通過可複用的類來封裝各種變化,讓變化的因素控制在最小的範圍內。
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 |
@interface ColorSpec() @property (nonatomic, assign) ProductColor color; @end @implementation ColorSpec + (instancetype)specWithColor:(ProductColor)color { ColorSpec *spec = [[ColorSpec alloc] init]; spec.color = color; return spec; } - (BOOL)satisfy:(Product *)product { return product.color == RED; } @end @interface BelowWeightSpec() @property (nonatomic, assign) float limit; @end @implementation BelowWeightSpec + (instancetype)specWithBelowWeight:(float)limit { BelowWeightSpec *spec = [[BelowWeightSpec alloc] init]; spec.limit = limit; return spec; } - (BOOL)satisfy:(Product *)product { return (product.weight < _limit); } @end |
使用者的介面也變得簡單多了,而且富有表現力。
[self findProducts:_products bySpec:[ColorSpec specWithColor:RED]];
這是經典的OO設計,如果熟悉設計模式的讀者對此已經習以為常了。設計模式是好東西,但往往被濫用。為此不能依葫蘆畫瓢,死板照抄,而是為了得到更簡單的設計而引入設計模式的,這個過程是很自然的。
與大師們交流,問究此處為何引入設計模式,得到的答案:直覺。忘記所有設計模式吧,管它是不是模式,如果設計是簡單的,這就是模式。
另外還有一個明顯的壞味道,ColorSpec和BelowWeightSpec都需要繼承ProductSpec,都需要定義一個建構函式和一個私有的欄位,並重寫satisfy方法,這些都充斥著重複的結構。
是不是覺得目前的寫法已經夠用了? 莫急, 讓我們來看看下個需求
- 需求4:查詢所有顏色為紅色,並且重量小於10的所有產品
Firth Attempt: Composite Criteria
按照既有的程式碼結構,往往易於設計出類似ColorAndBelowWeightSpec的實現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@interface ColorAndBelowWeigthSpec() @property (nonatomic, assign) ProductColor color; @property (nonatomic, assign) float limit; @end @implementation ColorAndBelowWeigthSpec + (instancetype)specWithColor:(ProductColor)color beloWeigth:(float)limit { ColorAndBelowWeigthSpec *spec = [[ColorAndBelowWeigthSpec alloc] init]; spec.color = color; spec.limit = limit; return spec; } - (BOOL)satisfy:(Product *)product { return product.color == _color || (product.weight < _limit); } @end |
存在兩個明顯的壞味道:
- 包含and的命名往往是違背單一職責的訊號燈
- ColorAndBelowWeightSpec的實現與ColorSpec,BelowWeightSpec之間存在明顯的重複
此刻,需要尋找更本質的抽象來表達設計,and/or/not語義可以完美解決這類問題。
- Composite Spec: AndSpec, OrSpec, NotSpec
- Atomic Spec:ColorSpec, BeblowWeightSpec
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 |
@interface AndSpec() @property (nonatomic, strong) NSArray *specs; @end @implementation AndSpec + (instancetype)spec:(ProductSpec *)spec, ... NS_REQUIRES_NIL_TERMINATION { va_list args; va_start( args, spec ); NSMutableArray *mArray = [@[spec] mutableCopy]; for ( ;; ) { id tempSpec = va_arg( args, id ); if (tempSpec == nil) break; [mArray addObject:tempSpec]; } va_end( args ); AndSpec *andSpec = [[AndSpec alloc] init]; andSpec.specs = [mArray copy]; return andSpec; } - (BOOL)satisfy:(Product *)product { for (ProductSpec *spec in _specs) { if (![spec satisfy:product]) { return NO; } } return YES; } @end |
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 |
@interface OrSpec () @property (nonatomic, strong) NSArray *specs; @end @implementation OrSpec + (instancetype)spec:(ProductSpec *)spec, ... NS_REQUIRES_NIL_TERMINATION { va_list args; va_start( args, spec ); NSMutableArray *mArray = [@[spec] mutableCopy]; for ( ;; ) { id tempSpec = va_arg( args, id ); if (tempSpec == nil) break; [mArray addObject:tempSpec]; } va_end( args ); OrSpec *orSpec = [[OrSpec alloc] init]; orSpec.specs = [mArray copy]; return orSpec; } - (BOOL)satisfy:(Product *)product { for (ProductSpec *spec in _specs) { if ([spec satisfy:product]) { return YES; } } return NO; } @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@interface NotSpec () @property (nonatomic, strong) ProductSpec *spec; @end @implementation NotSpec + (instancetype)spec:(ProductSpec *)spec { NotSpec *notSpec = [[NotSpec alloc] init]; notSpec.spec = spec; return notSpec; } - (BOOL)satisfy:(Product *)product { if (![_spec satisfy:product]) { return YES; } return NO; } @end |
可以通過AndSpec組合ColorSpec, BelowWeightSpec來實現需求,簡單漂亮,並且富有表達力。
[self findProducts:_products bySpec:[AndSpec spec:[ColorSpec specWithColor:RED], [BelowWeightSpec specWithBelowWeight:10], nil]];
但這樣的設計存在兩個嚴重的壞問道:
- AndSpec與OrSpec存在明顯的程式碼重複,OO設計的第一個直覺就是通過抽取基類來消除重複。
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 |
@interface CombinableSpec () @property (nonatomic, strong) NSArray *specs; @end @implementation CombinableSpec + (instancetype)spec:(CombinableSpec *)spec, ... NS_REQUIRES_NIL_TERMINATION { va_list args; va_start( args, spec ); NSMutableArray *mArray = [@[spec] mutableCopy]; for ( ;; ) { id tempSpec = va_arg( args, id ); if (tempSpec == nil) break; [mArray addObject:tempSpec]; } va_end( args ); CombinableSpec *combinableSpec = [[self alloc] init]; combinableSpec.specs = [mArray copy]; return combinableSpec; } - (BOOL)satisfy:(Product *)product { for (ProductSpec *spec in _specs) { if ([spec satisfy:product] == _shortcut) { return _shortcut; } } return !_shortcut; } @end |
1 2 3 4 5 6 7 8 9 10 |
@implementation AndSpec - (instancetype)init { self = [super init]; if (self) { self.shortcut = NO; } return self; } @end |
1 2 3 4 5 6 7 8 9 10 11 |
@implementation OrSpec - (instancetype)init { self = [super init]; if (self) { self.shortcut = YES; } return self; } @end |
- 大堆的初始化方法讓人眼花繚亂
[self findProducts:_products bySpec:[NotSpec spec:[AndSpec spec:[ColorSpec specWithColor:RED], [BelowWeightSpec specWithBelowWeight:10], nil]]];
Sixth Attempt: Using DSL
可以引入DSL改善程式的可讀性,讓程式碼更具表達力。
我們先新增一些DSL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static ProductSpec *COLOR(ProductColor color) { return [ColorSpec specWithColor:RED]; } static ProductSpec *BELOWWEIGHT(float limit) { return [BelowWeightSpec specWithBelowWeight:limit]; } static ProductSpec *AND(ProductSpec *spec1, ProductSpec *spec2) { return [AndSpec spec:spec1, spec2, nil]; } static ProductSpec *OR(ProductSpec *spec1, ProductSpec *spec2) { return [OrSpec spec:spec1, spec2, nil]; } static ProductSpec *NOT(ProductSpec *spec) { return [NotSpec spec:spec]; } |
這樣我們的程式碼表現起來就是這樣的
[self findProducts:_products bySpec:NOT(AND(COLOR(RED), BELOWWEIGHT(10)))];
Seventh Attempt: Using a Lambda Expression
可以使用Block改善設計,增強表達力。
1 2 3 4 5 6 7 8 9 10 |
- (NSArray *)findProducts:(NSArray *)products byBlock:(BOOL (^)())block { NSMutableArray *list = [@[] mutableCopy]; for (Product *product in products) { if (block(product)) { [list addObject:product]; } } return list; } |
程式碼現在開起來是這個樣子
[self findProducts:_products byBlock:^BOOL(id p) {return [p color] == RED;}];
構造DSL,複用這些Block
1 2 3 4 5 6 7 8 9 |
ProductSpecBlock color(ProductColor color) { return ^BOOL(id p) {return [p color] == color;}; } ProductSpecBlock weightBelow(float limit) { return ^BOOL(id p) {return [p weight] < limit;}; } |
1 2 3 4 |
- (void)test7_2 { [self findProducts:_products byBlock:color(RED)]; } |
Eighth attempt: Using NSPredicate
還可以使用標準庫
[self.products filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@”weight > 10″]];
結束
Dome在 WriteLeanPrograms目錄下
https://github.com/uxyheaven/SomeDemo
今天的編碼就到此為止了, 這篇文章本是Horance所寫, 筆者將用OC實現了一遍.如果我們們不是iOS Developer的話, 還是有其他attempt的, 如泛型.
作者介紹
劉光聰,程式設計師,敏捷教練,開源軟體愛好者,目前供職於中興通訊無線研究院,具有多年大型遺留系統的重構經驗,對物件導向,函式式,大資料等領域具有濃厚的興趣。
github: https://github.com/horance-liu
email: horance@outlook.com
邢堯, 資深開發工程師, iOS Developer, 開源軟體愛好者, 追求真理比佔有真理更加難能可貴
Github: https://github.com/uxyheaven
Blog: http://blog.csdn.bet/uxyheaven