阿金
curatorjin
早起赶画稿,熬夜写项目
disciple_sneaker@163.com
相关标签: 编程思维

粒度与可用性

什么是粒度

粗粒度的模块更容易使用,而细粒度的模块具有更高的可重用性

“粒度指的是一个系统要拆分成的各个部分的范围。粗粒度的模块会比细粒度的模块在行为上更为丰富。”

举个例子,假设现在要设计一个模块,对于给定A的输入,要得到E的输出。而由A到E需要经过四个步骤,那么伪代码可以写成:

public class Demo {
    public E process(A a) {
        B b = functionA(a);
        C c = functionB(b);
        D d = functionC(c);
        E e = functionD(d);
        return e;
    }
}

当中间过程ABCD都为私有方法或者直接写在了process方法中(相当于一个方法写到底),这时的process方法的粒度就是最大的,该方法更容易使用,一次调用就可以得到最终的结果。但是对于复用性而言,process方法也就只能做A到E的运算,而对于A到B或是C这种中间状态的运算就显得无能为力了。

私有的ABCD方法,粒度小,显得更为灵活。多个细粒度的方法可以组合成为不同的粗粒度方法。缺点也显而易见:使用的时候需要组合,不能够通过一次调用直接得到最终结果。也就是说,==细粒度的方法使用起来更为复杂==。

由此可见,粒度与可重用性之间是相互制约的。

可用性悖论与复杂性守恒

最大化重用会减少可用性

总体来说,==代码模块的可重用性越高,这个模块的使用门槛也就越高==。或者说是更为复杂:你需要调用多个细粒度的方法来得到你想要的的最终结果。可用性悖论指的是我们无法同时提高一个模块的可用性与重用性。对此我更喜欢理解为复杂性守恒。因为对于总体来说我们的运算过程是一致的。提高可用性也好,重用性也好,在做的都是平衡。

同样是前文中提到的例子,假设某一天我们有一个新需求:对于给定A的输入,要得到C的输出。这时我们不得不在Demo类中新增一个方法processNew,其中去调用functionAB,将所得到的的C返回。

此时你可能注意到:processNew方法正好对应了process方法中的前两步,那么出于减少代码重复度的考虑,可能会将process方法中的前两步直接替换成processNew方法。

然而从可用性与重用性的角度看,这样一通操作之后,其实只是新增加了一个由functionAB所组合成的新方法而已。其他方法的可用性与重用性仍旧保持不变。

复杂性守恒指的是,==对于业务处理而言,存在一个最小的处理流程,无论我们如何去权衡,整体的复杂度都不会小于这个最小处理流程的复杂度==。给定输入A,想要到达E,必须要经过BCD三个中间状态。也就是说,一个方法写到底的方案就是执行的最小流程。中间的调整只会使这个流程趋向复杂化。

既然如此,那么为什么还要去讨论这些问题呢?干脆都一个方法写到底不就好了。对于执行的机器而言,这当然是无所谓的。但是代码毕竟是写给人看的不是写给机器看的,重用性这个概念也是针对于开发者而不是执行机的。

设计思路对结构的影响

思考这样一个问题:代码结构是由什么决定的?设计思路?业务逻辑?还是说团队约定?

首先是设计思路,对于一个复杂的业务,我们一定会有的一个想法就是分而治之。如何理解业务、拆分业务决定了我们总体的业务架构和业务分工。有了架构,有了分工再考虑每个分工下的业务逻辑(实际上这里还是分而治之的思想),再细分为更小的模块。再到业务实现时,团队之间的配合、约定又形成了这一阶段的结构。

结论是:==业务逻辑决定设计方向,设计思路决定整体架构,团队约定解决合作冲突==。

回到可用性与复用性的问题上,代码设计时,我们想要其具有更高的重用性,又希望它尽可能的简单易读。模型化、抽象化、结构化甚至是高内聚低耦合的标准都是这一思想的体现。在这个前提下,应该思考的就不只是简单的逻辑实现,应该把重点放在整体的设计与后续的发展上。

总结:权衡与设计

鱼和熊掌不可兼得。

可用性与重用性对于系统整体的最优解并不唯一,其中的权衡值得我们一直思考。

参考资料