多数程序员难以用简单的方式开发应用?

阅读数:18542 2019 年 8 月 22 日 13:58

多数程序员难以用简单的方式开发应用?

心理学中有一篇相当古老、但又非常重要的论文,题为《魔法数字七(上下浮动二):人类信息处理能力中的一些限制》。这篇文章衡量了大脑处理信息的极限,并给出了一个具体的数字:人脑可以同时容纳五到九个概念。我们当然能够把这个有趣的结论延伸到诸多领域当中,但对软件开发人员而言,下面两项含义最为重要:

  • 构造(模型、实现、设计、模式等……)越简单越好,因为其概念更易于描述。
  • 构造良好且组合良好的抽象,应该具有较少的特殊规则与意外情况,同样是因为其概念更易于描述。

多数程序员难以用简单的方式开发应用?

总而言之,这是一场以降低概念描述难度为目标的心理战,夺取的是珍贵的心理容纳空间。

简单构造

正如英国计算机专家 Hoare 所言:软件设计构建有两种方法,一是使其尽可能简单,从而一目了然确定其中不存在缺陷;另一种方法则是使其极为复杂,以至于看不出什么明显的缺陷。

遗憾的是,他还补充道:第一种方法其实要困难得多。

在软件开发当中,添加新的模块、新的类以及越来越多的代码是件很简单的事。这能够解决软件需要面对的更多用例,但更大的代码量所产出的结果却越来越少。正如 Jamie Zawinski 所说:每一个程序都会不断扩展,一直扩展到像人一样会收邮件、看邮件。而那些无法扩展的程序,最终会被能够扩展的程序所取代。

从本质上讲,我们一直在不断扩展程序。我们向其中添加更多模块、更多类以及更多代码,而且对于每一段代码,我们都需要确保其能够与原有代码顺畅协作,从而在这个复杂性呈指数增长的过程中继续保持进一步扩展的空间。之所以会这样作,是因为增量总是更简单的——至少在起步阶段更简单。

在另一方面,越是简单的东西开发起来就越困难。这要求开发人员考虑潜在的模式与行为,并消除一切不必要的元素。但最终,这样的思维方式能够成就更出色的成果,因为我们只需要使用极少的概念与精力,就可以描述并理解程序,确保大家专注于更多宏观目标、而非细枝末节。

检测过于复杂的构造

实际上,发现这类复杂而臃肿的构造并不困难。我们看看程序手册就能知道个大概,如果其中有专门的章节来描述结构背景之下的众多词汇,基本可以断定程序复杂度已经失控。

下面来看一个例子,即 Abstract Factory(抽象工厂)模式。为了描述其工作原理,我们需要定义很多名称:

  • AbstractFactory 接口
  • <<creates>>活动
  • Factory1(或者 ConcreteFactory)
  • ProductA 以及 ProductBinterfaces
  • ProductA1 以及 ProductB1classes
  • new 关键字
  • Abstract Factory 类图
  • :Client 以及 factory:Factory1 序列图

之所以存在这么多名称与图类,是因为复杂性比简单性更易于实现。这也迫使程序员采用针对性过强的对象创建方式,而无法选择复用存储在某处的对象等其它可能的解决方案。通过以下方式,我们尝试降低其整体复杂性:

  • 从整体模式当中删除“Factory”名称。
  • 删除 new 关键字以及 <<creates>>活动。

现在,该模式只关注能够返回 ProductA 或者 ProductB 实例的对象。它可以对该类进行实例化、从数据结构中获取预先构建的对象,或者从某处提取该对象——具体方法无穷无尽,但这些都不重要;真正重要的是我们的目标:开始时没有对象,结束时有了对象。现在再来看名称列表,两项操作:

  • 第一项,利用接口 ProductA 获取一个对象。
  • 第二项,利用接口 ProductB 获取一个对象。

这样,我们可以自由地将该操作放在某个类、某个结构或者是某个指针当中。在使用时,我们只需要配合必要的参数对该操作进行调用,它就会为我们提供相应的对象。

在设置中,我们可以将第一项操作设置为任何能够生成 ProductA 的形式,并将第二项操作设置为任何能够生成 ProductB 的形式。没有类图、没有序列图、没有额外的类、没有不必要的字,也没有歧义——因为我们能够对某项操作做的只有调用,将其作为参数进行传递,获取返回结果并加以存储。毕竟函数与操作在本质上就是带有参数的值。我们要如何利用接口 ProductA 获取某个对象?如何才能返回一个 ProductA?答案很简单:使用第一项操作。

构造良好的抽象

我们已经在 Abstract Factory 的修正版中描述了拥有良好构造的抽象结构。

构造良好的抽象

AbstractFactory 模式显然不是什么好抽象,因为它包含一切开发者不关心、也没必要关心的繁琐细节——例如属于抽象、必须创建对象,以及使用 new 关键字等。作为用户,我并不关心这么多细节,但这些细节就在眼前,想躲都躲不开。

相反,我只希望拥有“一项能够利用 ProductA 接口为获取对象的操作。”我只需要进行调用,它就能返回 ProductA 的结果。至于具体获取方式,我不知道、也不在乎。

组合良好的抽象

AbstractFactory 模式不是什么好抽象,因为它没有得到良好组合。看看它用到的两种方法:其一是创建 ProductA;其二是创建 ProductB。为什么要把它们放在同一个 Factory 当中?为什么不能把这两项操作分开,并让希望将二者一同使用的用户将其放进同一个元组中?A 和 B 在同一个类中,绝对不是什么良好的构造方式。我该如何组合?如果其它地方也有需要使用 ProductA 的操作怎么办?那项操作是否需要了解 AbstractFactory 中的错误链?

复制代码
AbstractFactory myFactory = new Factory1();
ProductA myProduct = myFactory.createProductA();
AbstractFactory somethingDifferent = new SomethingDifferentFactory();
SomethingDifferent result = somethingDifferent.fabricate(myProduct);
return result

相反,如果拥有“一项能够利用接口 ProductA 为我获取对象的操作”,而另有一项操作要求使用 ProductA 以构造别的结果,那就可以调用第一项操作来获取 ProductA,然后再用它来执行第二项操作。在类型方面:

  • operation : () -> ProductA
  • otherOperation: ProductA -> SomethingDifferent

因此: otherOperation(operation())。完事,就这么简单。

检测组合性是否良好

遗憾的是,很难通过语义检测组合性是否良好,因为开发人员已经习惯了那些没有组合性可言的做法,因此很多人压根不知道所谓组合良好究竟是什么概念。好的组合其实能够轻松感受到,这有点像是乐高积木,每一块都能轻松与其它块匹配起来,共同搭建出规模可观的作品。

这块有个三角形尖头,我们看看哪些积木块有三角形开孔。

当具有良好的组合性时,我们不需要使用适配器(或者不会设置适配器这样的概念,因为连接机制本身非常简单)。当具有良好的组合性时,我们只需要将各部分连接起来即可构建成一个整体,而不必担心这些部分会以意想不到的方式相互作用。
其整体,就是各部分相加得出的精确总和,仅此而已。

总结

我们已经看到了 AbstractFactory 如何破坏“获取 ProductA”这样一个简单的概念:一个 Factory 即为一个 Abstract,其通过 createProductA 方法利用 new 以 <<construct>>一个拥有 ProductA <<interface>>的 ProductA1。

这里单是关键字与概念就多达 10 个,超出了最聪明的大脑所能轻松处理的极限。更糟糕的是,由于太过刻板,我们只能使用这项操作创建对象、而无法复用对象。这也是编程为何如此困难的原因之一:我们毫无意义地抬高了它的难度。

原文链接:
http://www.javiercasas.com/articles/rule-of-seven

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论