8次嘗試,帶你走進iOS 精益編程
開場
今天, 我們將從一個小功能開始, 先去不假思索的實現(xiàn)它
Product Repository: Filtering Operation
Code start
有一個產(chǎn)品庫, 我們要對它做過濾操作.
***個需求并不復雜.
需求1:在倉庫中查找所有顏色為紅色的產(chǎn)品
First Attempt: Hard Code
我們先用最簡單的方式去實現(xiàn)它, 硬編碼
- - (NSArray *)findAllRedProducts:(NSArray *)products
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.color == RED) {
- [list addObject:product];
- }
- }
- return list;
- }
如果這個世界是永恒靜止的,這樣的實現(xiàn)無可厚非,但世界往往并非如此。
緊接著,第二個需求來了
需求2:在倉庫中查找所有顏色為綠色的產(chǎn)品
Second Attempt: Parameterizing
Copy-Paste是大部分程序員最容易犯的毛病,為此引入了大量的重復代碼。
- - (NSArray *)findAllGreenProducts:(NSArray *)products
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.color == GREEN) {
- [list addObject:product];
- }
- }
- return list;
- }
為了消滅硬編碼,得到可重用的代碼,可以引入簡單的參數(shù)化設(shè)計。
- - (NSArray *)findProducts:(NSArray *)products byColor:(ProductColor)color
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.color == color) {
- [list addObject:product];
- }
- }
- return list;
- }
終于可以放心了, 這個時候我們的產(chǎn)品經(jīng)理怎么可能讓你舒服呢,需求3又來了
需求3:查找所有重量小于10的所有產(chǎn)品
Third Attempt: Parameterizing with Every Attribute You Can Think Of
大部分程序員依然會使用Copy-Paste解決這個問題,拒絕Copy-Paste的陋習,***實效的一個反饋就是讓這個快捷鍵失效,從而在每次嘗試Copy-Paste時提醒自己做更好的設(shè)計
- - (NSArray *)findProducts:(NSArray *)products byWeith:(float)weight
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.weight < weight) {
- [list addObject:product];
- }
- }
- return list;
- }
為了消除兩者重復的代碼,通過簡單的參數(shù)化往往不能***解決這類問題,相反地會引入過度的復雜度和偶發(fā)成本。
- - (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;
- }
日常工作中,這樣的實現(xiàn)手法非常普遍,函數(shù)的參數(shù)列表隨著需求增加不斷增加,函數(shù)邏輯承擔的職責越來越多,邏輯也變得越來越難以控制。
通過參數(shù)配置應(yīng)對變化的設(shè)計往往都是失敗的設(shè)計
易于導致復雜的邏輯控制,引發(fā)額外的偶發(fā)復雜度
Forth Attempt: Abstracting over Criteria
為此需要抽象,使其遍歷的算法與查找的標準能夠獨立地變化,互不影響。
- @interface ProductSpec : NSObject
- - (BOOL)satisfy:(Product *)product;
- @end
此刻filter的算法邏輯得到封閉,當然函數(shù)名需要重命名,使其算法實現(xiàn)更加具有普遍性。
- - (NSArray *)findProducts:(NSArray *)products bySpec:(ProductSpec *)spec
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if ([spec satisfy:product]) {
- [list addObject:product];
- }
- }
- return list;
- }
通過可復用的類來封裝各種變化,讓變化的因素控制在最小的范圍內(nèi)。
- @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
用戶的接口也變得簡單多了,而且富有表現(xiàn)力。
- [self findProducts:_products bySpec:[ColorSpec specWithColor:RED]];
這是經(jīng)典的OO設(shè)計,如果熟悉設(shè)計模式的讀者對此已經(jīng)習以為常了。設(shè)計模式是好東西,但往往被濫用。為此不能依葫蘆畫瓢,死板照抄,而是為了得到更簡單的設(shè)計而引入設(shè)計模式的,這個過程是很自然的。
與大師們交流,問究此處為何引入設(shè)計模式,得到的答案:直覺。忘記所有設(shè)計模式吧,管它是不是模式,如果設(shè)計是簡單的,這就是模式。
另外還有一個明顯的壞味道,ColorSpec和BelowWeightSpec都需要繼承ProductSpec,都需要定義一個構(gòu)造函數(shù)和一個私有的字段,并重寫satisfy方法,這些都充斥著重復的結(jié)構(gòu)。
是不是覺得目前的寫法已經(jīng)夠用了? 莫急, 讓我們來看看下個需求
需求4:查找所有顏色為紅色,并且重量小于10的所有產(chǎn)品
- Firth Attempt: Composite Criteria
- 按照既有的代碼結(jié)構(gòu),往往易于設(shè)計出類似ColorAndBelowWeightSpec的實現(xiàn)。
- @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的實現(xiàn)與ColorSpec,BelowWeightSpec之間存在明顯的重復
此刻,需要尋找更本質(zhì)的抽象來表達設(shè)計,and/or/not語義可以***解決這類問題。
Composite Spec: AndSpec, OrSpec, NotSpec
Atomic Spec:ColorSpec, BeblowWeightSpec
- @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
- @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
- @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來實現(xiàn)需求,簡單漂亮,并且富有表達力。
- [self findProducts:_products bySpec:[AndSpec spec:[ColorSpec specWithColor:RED], [BelowWeightSpec specWithBelowWeight:10], nil]];
但這樣的設(shè)計存在兩個嚴重的壞問道:
AndSpec與OrSpec存在明顯的代碼重復,OO設(shè)計的***個直覺就是通過抽取基類來消除重復。
- @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 = [[CombinableSpec 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
- @implementation AndSpec
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- self.shortcut = NO;
- }
- return self;
- }
- @end
- @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:
- 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];
- }
這樣我們的代碼表現(xiàn)起來就是這樣的
- [self findProducts:_products bySpec:NOT(AND(COLOR(RED), BELOWWEIGHT(10)))];
- Seventh Attempt: Using a Lambda Expression
可以使用Block改善設(shè)計,增強表達力。
- - (NSArray *)findProducts:(NSArray *)products byBlock:(BOOL (^)())block
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (block(product)) {
- [list addObject:product];
- }
- }
- return list;
- }
代碼現(xiàn)在開起來是這個樣子
- [self findProducts:_products byBlock:^BOOL(id p) {return [p color] == RED;}];
構(gòu)造DSL,復用這些Block
- ProductSpecBlock color(ProductColor color)
- {
- return ^BOOL(id p) {return [p color] == color;};
- }
- ProductSpecBlock weightBelow(float limit)
- {
- return ^BOOL(id p) {return [p weight] < limit;};
- }
- - (void)test7_2
- {
- [self findProducts:_products byBlock:color(RED)];
- }
- Eighth attempt: Using NSPredicate
還可以使用標準庫
- [self.products filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"weight > 10"]];
結(jié)束
今天的編碼就到此為止了, 這篇文章本是Horance所寫, 筆者將用OC實現(xiàn)了一遍.如果咱們不是iOS Developer的話, 還是有其他attempt的, 如泛型.