代码工匠

Walking The Long Road.

没有银弹-谈谈软件设计的几个矛盾

最近在做项目的重构和功能改进,设计做了很多,也发生了一些争执。其实总结下来,很多争执的内容其实早就是经典的问题。这些问题没有孰优孰劣,具体采用哪种方案,还得因地制宜,详细分析项目需求和复杂度之后,再做决定。之前很多人都试图只从宏观指导思想来决定设计,最后大家谁也不服谁,所以先把问题确定下来,至少以后思考问题会直接一点。

1. 拆分与合并

从现实世界来说,事物本身就是互相联系的,从这个观点来看,任何对事物的拆分都是不完全正确的。

但是软件开发中,人的理解能力是有限的,而拆分目前看来是降低单个项目复杂度最有效的办法。

拆分有很多级别,最小的可能是拆分代码段,用多个函数代替单个函数,然后是用多个类代替单个类,在Java里面,还可以拆分package,然后拆分jar包,最后拆分成不同的项目。

之前有过很多的争执,关于一个项目要拆还是不拆,以及如何拆。关于这个,我的建议是:

  1. 拆与不拆没有对错

    Windows是微内核架构,Linux是单内核架构。微内核意味着内核很小,你可以通过很多个模块去补充它,内核与模块是解耦的。Linux是单内核,就表示所有内核功能会在编译时就确定。可能大家都觉得微内核更好,很多时候它确实更好,但是Linus有个经典的论断:“你不需要管理各个模块,但是你需要处理模块之间的依赖,这个可能比模块本身更复杂”。因为事物本身就是互相联系的,你觉得他们不存在耦合,只是当前使用场景用不到而已。

  2. 系统内部实现对外部透明,保留拆或者不拆的选择权。

    项目自身的复杂度,完全可以靠内部实现解决,对外保持约定好的API,这样对于以后内部的重构,会简单得多。相反,如果暴露了内部实现,那么修改就很困难了。

  3. 对于项目拆分,如果没有充足的理由支持拆分,就不要拆。

    不成熟的拆分,最常见的结果是,随着需求的变化,你不得不打破这种解耦关系,这样反而会带来更多的问题。建议是需求稳定之后,再考虑拆分。

  4. 在系统内部多多进行代码级别的拆分,管理复杂度。

    相比项目的拆分,函数和类级别的拆分成本非常低,值得多用。

2. 配置化与灵活性

一段代码,如果使用一遍,那么我们就直接通过代码实现了。如果我们有几十上百个类似的任务,那么我们就不希望写重复的代码了,我们希望能够通过配置几个不同的参数,从而实现不同的任务。如果任何以后还有不断变化的需求,我们甚至不希望自己写配置,而是有一个运营后台来让需求方(可能是不懂开发的人)直接完成配置。

配置化的开发方式往往对开发者来说有很大的诱惑,从而忽略其中的成本,这个配置最近还有个很火的名字,叫做DSL。但其实配置化和灵活性是矛盾的,配置的表述能力自然要弱于通用语言。当然,也有人尝试使用配置解决所有问题,结果只不过是发明了一门很难用的语言而已。

我自己的框架WebMagic是一个经典的配置与灵活性权衡的例子。WebMagic是一个垂直爬虫框架,爬虫最复杂的是规则的编写,你可以认为这是一个可配置的东西。公司基于它做了一个配置后台,即使是这样,仍然有一些情况,不得不手写Java代码来实现一些功能。

对于这个问题,我的建议是:

  1. 先写代码解决问题,但是提前约定接口。

    第一个阶段,没有谁能预测以后的需求,所以先用你熟悉的代码实现。可以根据你的输入和输出,约定程序级别的接口,相比配置化,这一般来说会容易,如果接口设计得当,也会有具有很大灵活性,以后基本无需更改。

  2. 在有一定积累之后,基于以往的任务做配置化。

    配置的内容是什么呢?首先公共逻辑肯定会在整体框架中,配置的内容应该是不同任务彼此独特的部分。这个配置格式,或者DSL的语法的约定,首先应该基于已有的任务,然后可能考虑一下未来的情况。我是个实践主义者,所以我更多的会参考已有的情况,如果发现这个配置化的框架,对之前的任务都不能满足,那么就需要思考一下它的可行性和必要性了。

  3. 在任何时候都保留能使用代码实现的能力。

    我是个实用主义者。有了配置,如果不提供代码实现的能力,而又有一些复杂的需求,那么就只能扩展配置的能力了。这样只可能会导致这个配置解决框架变得极其庞大和复杂,而相对收益却很低。这个时候,可以通过配置解决大部分问题,然后通过代码解决少量问题,也是不错的选择。

3. 总结

其实还有很多东西没说到,以后补充吧。

总结一句话:软件设计要适应满足需求,同时不断演化。

Add a comment