Mar 1

“面向鸭子编程”(Duck Test)实践

Lrdcq , 2019/03/01 23:43 , 雜談 , 閱讀(4247) , Via 本站原創
Duck Test,一种著名的归纳原理,就是说“如果它看起来像鸭子,游泳像鸭子,叫声像鸭子,那么它可能就是只鸭子。”,不浮于面向协议编程这种标准的编程形式,“面向鸭子编程”的思路旨在以事实,而不只是约定为标准,进行代码实践。并不是说鸭子测试的思路和面向协议编程是矛盾的,鸭子测试依然是基于协议的,但它的核心是不盲信任于任何协议或者约定,而是基于事实的代码检测去完成程序逻辑

举个栗子

抛去类似于java这种在面向对象这块严谨上天了的编程语言,很多语言都有类似的行为。

比如我们熟悉的oc中,我们知道,一个要支持setter自动copy的类型,需要实现NSCopying协议或者NSMutableCopying协议。并且实现对应的- (id)copyWithZone:(NSZone *)zone方法与- (id)mutableCopyWithZone:(NSZone *)zone方法。这样,我们就可以在@property上正常使用copy,每次使用的时候,会正常调用copy方法并且执行实现的方法。

但是,如果我们不声明NSCopying,只需要实现实现copy的方法,就可以正确执行执行@property的copy功能,甚至编译时不会出现warning。这样的类的定义方式也被成为Duck Typing

同理,我们的数组可以通过for in去快速遍历,并且是因为它实现了协议NSFastEnumeration。当然可以试试,我们一个类只是实现协议的方法- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)length,别的什么都不干,也可以通过for in进行遍历了。

熟悉oc的同学肯定理解,我们正常使用协议的时候,肯定不会特意conformsToProtocol去判断目标对象是否声明协议,而是直接调用respondsToSelector去判断目标对象是否实现协议中必须要实现的方法

这就是Duck Test。

另一个更自然的例子是,在oc中,只要实现- (T)objectForKeyedSubscript:(T)key和- (void)setObject:(T)obj forKeyedSubscript:(T)key两个方法就可以在代码中使用x[T]的方式进行读写,并且这个东西并没有真正的Protocol,而是基于文档/口头约定的协议去完成的。而苹果的验证方式,也肯必然是基于方法是否真正实现来做判断的。

举个栗子2

在日常的业务开发/代码实践中,我们也常有类似的方案。比如非常常见的场景为复杂列表/feed流区域。

从后端下发的会多多种类型的实体,class A/B/C/D/E/F/G.../Z,而我们通过数据注册和获取item的事后,往往会这么写:
//注册item
[_tableview registerClass:[ACell class] forCellReuseIdentifier:NSStringFromClass(A.class)];
[_tableview registerClass:[BCell class] forCellReuseIdentifier:NSStringFromClass(B.class)];
...
[_tableview registerClass:[ZCell class] forCellReuseIdentifier:NSStringFromClass(Z.class)];
//使用item
id data = _list[indexPath.row];
BaseCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(data.class) forIndexPath:indexPath];
cell.data = data; //BaseCell的属性
return cell;

在这个场景里,我们根据数据去映射得到具体的cell,这样界面完全是根据存在的数据去描述成的。这里那一摞registerClass只是描述的这个列表可能存在的数据的约定,实际展示的页面item只和实际存在的数据有关

如果说上面这个栗子的约定约束性还是太强了,另外一个在dsl级动态化方案中的写法是:

1. 动态实体,实体并没有描述为一个具体的class ABC,而是就像js一样存在于map中。
2. 每个cell渲染时,根据map中约定的数据去远端获取动态布局。
3. 根据这个数据map与动态布局去渲染出整个cell。

约定依然存在:一个实体对应一个动态布局,只是这个约定不一定是在客户端上的约定。另外也没有约定实体为特定的实体。

总结与实践方式

可以看到,Duck Test依然存在非常核心的约定:

1. 这种编程理念的核心是,描述约定“什么是鸭子”即“看起来怎样,游起来怎样,叫起来怎样”。而代码在使用的时候,不是基于声明“我是鸭子”,而是基于以上的描述约定去判断。约定协议可以在代码中正式存在,也可以基于文档,注释,数据描述。

2. 这个写法的最大的特点是,代码不依赖于代码级协议。可以非常方便的解耦模块化拆分配置化/动态化

3. 缺点也很明显,按场景来看并不是非常通用的,很多场景有过度封装的嫌疑,同时代码复杂度/逻辑复杂度也会大大的增加。如果要做到极限/普遍性的鸭子测试,所有场景的测试量均一致,并且对于一个客户端来说,是非常巨大的工作量。因此实际实现上肯定有很多取舍。另外,由于在程序中带来一定量的隐式行为,基于程序以外的协议/文档建设需要更加重视。

4. 基于以上总结与既有的实践经验,很明显,实践上,基于鸭子的约定的场景更适合在偏向基础设置/通用能力/标准化的场景,去做广泛,一致性,并面向切片的标准逻辑。而不适合本身业务抽象复杂,客户端能力要求高或者高频调用对性能有极限要求的场景。
关键词:鸭子
logo