【死磕iOS】8 次尝试,带你走进 iOS 精益编程

转自:http://www.cocoachina.com/ios/20151026/13884.html

开场

今天, 我们将从一个小功能开始, 先去不假思索的实现它

  • 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

@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

用户的接口也变得简单多了,而且富有表现力。


1

[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

@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

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

@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来实现需求,简单漂亮,并且富有表达力。

[self findProducts:_products bySpec:[AndSpec spec:[ColorSpec specWithColor:RED], [BelowWeightSpec specWithBelowWeight:10], nil]];

1

但这样的设计存在两个严重的坏问道:

  • 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

40

41

42

43

44

45

46

47

48

49

50

51

@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

  • 大堆的初始化方法让人眼花缭乱

1

[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

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];

}

这样我们的代码表现起来就是这样的


1

[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;

}

代码现在开起来是这个样子


1

[self findProducts:_products byBlock:^BOOL(id p) {return [p color] == RED;}];

构造DSL,复用这些Block


1

2

3

4

5

6

7

8

9

10

11

12

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

还可以使用标准库


1

[self.products filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"weight > 10"]];

结束

今天的编码就到此为止了, 这篇文章本是Horance所写, 笔者将用OC实现了一遍.如果咱们不是iOS Developer的话, 还是有其他attempt的, 如泛型.

作者介绍

  • 刘光聪,程序员,敏捷教练,开源软件爱好者,目前供职于中兴通讯无线研究院,具有多年大型遗留系统的重构经验,对面向对象,函数式,大数据等领域具有浓厚的兴趣。
  • 邢尧, 资深开发工程师, iOS Developer, 开源软件爱好者, 追求真理比占有真理更加难能可贵
时间: 2024-10-06 08:53:09

【死磕iOS】8 次尝试,带你走进 iOS 精益编程的相关文章

死磕 Fragment 的生命周期

死磕 Fragment 的生命周期 本文原创,转载请注明出处. 欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文. 本文例子中 github 地址: github 项目链接 曾经在北京拥挤的13号线地铁上,一名背着双肩包穿着格子衫带着鸭舌帽脚踏帆布鞋的程序员讲了一句: "我觉得 Fragment 真的太难用了".从而引起一阵躁动激烈的讨论. 正方观点: Fragment 真的太好用了.要知道因为 Activi

死磕 java线程系列之线程池深入解析——未来任务执行流程

(手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类. 简介 前面我们一起学习了线程池中普通任务的执行流程,但其实线程池中还有一种任务,叫作未来任务(future task),使用它您可以获取任务执行的结果,它是怎么实现的呢? 建议学习本章前先去看看彤哥之前写的<死磕 java线程系列之自己动手写一个线程池(续)>,有助于理解本章的内容,且那边的代码比较短小,学起来相对容易一些. 问题

【死磕Java并发】-----J.U.C之重入锁:ReentrantLock

此篇博客所有源码均来自JDK 1.8 ReentrantLock,可重入锁,是一种递归无阻塞的同步机制.它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更强大.灵活的锁机制,可以减少死锁发生的概率. API介绍如下: 一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大.ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥

【死磕Java并发】-----J.U.C之AQS:CLH同步队列

此篇博客所有源码均来自JDK 1.8 在上篇博客[死磕Java并发]-–J.U.C之AQS:AQS简介中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态. 在CLH同步队列中,一个节点表示一个线程

【死磕Java并发】-----J.U.C之读写锁:ReentrantReadWriteLock

此篇博客所有源码均来自JDK 1.8 重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少.然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低.所以就提供了读写锁. 读写锁维护着一对锁,一个读锁和一个写锁.通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞. 读写锁的主要特

makefile死磕笔记

开始我会插播一段我如何学习makefile的废话,如果不想听的话,请直接跳到我的makefile教程. 首先得先说明学习makefile真是一个痛苦的过程,尤其是用干巴巴的看书来学习的过程,简直可以用如坐针毡来形容了……不过作为一个想成为真正程序员的人这又算得了什么呢?为了不被人诟病编程只会用IDE,你得硬着头皮来学习这个让人痛苦的东西,好在有一句话,痛苦是进步的标识,这至少说明了你在进步,也挺好的. 通过这几天的痛苦学习,我觉得学习makefile得分这么几个步骤: 1.熟悉大概的makefi

离开华为三年,我才真正认同狼性法则(目标导向,没有借口,都是为自己的懒惰与不肯死磕找借口)

离开华为三年,我才真正认同狼性文化 推荐语: 我从在爱立信,到华为,到完全脱离通信自己创业,曾经在MSCBSC论坛也写出了对通信行业,对网优的深刻认识,到对比华为和爱立信企业文化的优劣,这次,再对自己创业踩坑后的回顾. 创业艰难,人生总是在不断试错的过程中成长,作为他的好友,佩服他的勇气,祝福他的成长,加油吧兄弟. ——邓志强 三年前的现在,我离开了华为,裸辞. 彼时,这家公司已然处在电信业塔尖. 5月,我就已提离职.拖到9月,是因为领导希望我能待到下半年,替部门背一个考核C的指标. 华为绩效考

【正文】Java类加载器( CLassLoader ) 死磕 4: 神秘的双亲委托机制

[正文]Java类加载器(  CLassLoader ) 死磕4:  神秘的双亲委托机制 本小节目录 4.1. 每个类加载器都有一个parent父加载器 4.2. 类加载器之间的层次关系 4.3. 类的加载次序 4.4 双亲委托机制原理与沙箱机制 4.5. forName方法和loadClass方法的关系 4.6. 使用组合而不用继承 4.7. 各种不同的类加载途径 4.1.每个类加载器都有一个parent父加载器 每个类加载器都有一个parent父加载器,比如加载SystemConfig.cl

死磕 java集合之DelayQueue源码分析

问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于实现定时任务. 继承体系 从继承体系可以看到,DelayQueue实现了BlockingQueue,所以它是一个阻塞队列. 另外,DelayQueue还组合了一个叫做Delayed的接口,DelayQueue中存储的所有元素必须实现Delayed接口. 那么,Delayed是什么呢? public