代码是如何生长的

原创
2018/05/17 14:27
阅读数 113

2014年,我还是一名大学生,在兴趣的驱使下走上了编程的道路。后在各种洪荒之力的推动下于2016年7月开始耗费半年多时间编写了一个叫做miniqueue的网站,项目虽然是失败的,不过经验是宝贵的。完成这个网站后,我写下了下面这些文字,它本已被我遗忘在脑海,今天整理电脑才又发现了它,读之,感觉颇有受益,在此分享,给学习编程的新手们,或是如我一般在个中摸爬滚打过的猿们。

代码是如何生长的

这是一篇对miniqueue开发以来的总结性论文。

设计是一个险恶的问题:

软件编程中,最重要的一环可以说就是软件设计,也叫软件架构,这是决定一个软件质量优劣的最根本一环。这一点从程序员的发展规划中就可见一斑(顶尖程序员往往冠以“软件架构师”一名)。通过这个软件,我有幸体验了一次糟糕设计下的编程,并逐步重构,转向良好设计。

设计是一个险恶的问题。根据Horst Rittel和Melvin Webber的定义,“险恶的问题,就是那种只有通过解决或部分解决才能被明确的问题。”这个看似矛盾的定义其实是在说,你必须通过把某个问题解决一遍,才能明确的定义它,然后再次解决,以形成一个可行的方案。

现实世界中一个极好的例子就是Tacoma Narrows大桥。这座位于美国华盛顿州塔科马的悬索桥,于1940年一个狂风大作的上午,由于在风力作用下摇晃剧烈而坍塌。直到这座大桥的坍塌,工程师们才知道应该充分考虑空气动力学因素。只有通过建造这座大桥(即解决这个问题),他们才能学会从这一问题中应该考虑的额外因素。

miniqueue的后台在早期阶段,沿用了制作小型程序的办法,即没有太多的考虑系统的层次性和模块性,一切以实现功能为主。在数据库和界面之间,只有一层粗糙的数据库操作层。这时的系统开发速度极快,因为它实际上忽视了很多关键问题。随着系统的不断扩展,这个数据库操作层变得越来越臃肿,而且难以维护。比如,每个数据库的表在数据库操作层都映射了一个类,但表和表的关系有时并不是独立的,有些表需要相互协作,以完成某些操作,此时联合的行为就被推到了界面层,这导致界面层本来用以调度页面,却附带要处理很多业务逻辑的问题。

于是我着手重构系统,在新设计的系统里,我在数据库操作层之上,界面层之下,添加了一个层,叫做系统层。系统层将会调用数据库操作层的接口,以实现数据库各个表的相互协作,由此它可以给界面层提供一套更加抽象的接口,这套抽象可以很好的隐藏数据库结构,客户看到的将是一个完整的系统。如,节点系统(TableSystem)设计了两个类——TableOperateSystem和TableFindSystem,其中TableOperateSystem就提供了一个创建新节点的接口createTable()。但节点系统并不映射那张叫做Table的数据库表,因为创建新节点这个动作实际上涉及很多张表的改动。如此一来,底层的数据库操作就和顶层的界面分离了,现在界面层仅和系统层打交道。

通过这一步重构,我还发现了一个额外的问题,系统的逻辑应该由系统层来接手。你很难想象,初版的系统尽然是由界面来处理整个系统逻辑的。你可以干什么,不可以干什么,全由界面说了算。因为整个底层将数据库操作的接口都暴露了。如果我不进行这次重构,我甚至都不会发现这个问题的存在,正是由于系统层的出现,使得我对整个系统的理解更加深刻,明了,才促成了这一意外发现。

然而好景不长,在系统层,那个老问题再次显现——表与表的关系有时是相互关联的,这意味着,底层数据库操作层的某些类会在系统层纠缠在一起,这种纠缠带来的某些接口的联合调用会重复的出现在系统层的不同位置,这显然不是我们所想看到的。于是另一个层显现了,此时思路变得非常清晰,明了。我们缺一个数据模块层,用来将底层的数据库操作层模块化,提供某种比离散的数据库操作更加统一的接口,以便消除系统层里的接口纠缠!到目前为止,系统被设计成了四个层次,由顶向下分别是界面层,系统层,数据库模块层,数据库操作层。

在没有解决这个问题前,我认识不到系统分层的优势,也许从各种书籍中读到了相关内容,但没有实践经验的支撑,那些文字描述在我的脑海里始终是个空壳,而现在,什么是分层,什么是模块化,我已经有了全新的认识。

看清类的合适粒度:

类应该有多大?在极端条件下,一个类可以吃下整个系统的全部代码,或者各个类有且仅有一个接口,这样每个类就相当于一个方法。这两种情形就是类的两种极端粒度,它们都不是我们想要看到的。那么问题出现了,类应该有多大?

软件设计原则中,有一条“单一职责原则”,简称SRP,它描述的是,类的职责要单一,不能将太多的职责放在一个类中。在没有经验支撑的情况下,它不过是一句空话,因为它什么也没告诉你,当你面对一个系统的时候,你还是不知道一个类做到何种程度算是满足了单一职责原则。

这个问题在做miniqueue的时候始终困扰着我。在最初的设计中,数据操作层里的每个类,对应一张数据库表,我认为这个类就满足单一职责了,因为它只对一个表负责。在开发初期,这些类确实可用,它们的粒度算是合适的,没有对我造成什么困扰。然而随着系统的发展,这个类开始“爆炸”了。由于它处理了数据库的增删改查四个功能,所以每当我需要它满足一个新的功能的时候,它都需要添加一项新的接口,于是我得再一次深入类的内部去编程。这时你会发现你违反了另一项原则——“开闭原则”简称OCP。“开闭原则”说的是,软件实体应该对扩展开放,对修改封闭。什么是扩展,什么是修改?这个问题我也是在不断的开发中才切实领会的!具体来说,软件开发应该以类为最小单位,当一个类投入使用,它就不应该再被修改,注意这里的修改指的是这个类的接口,也就是它所有的public成员。

将类视为最小单位意味着,当你编写或者修改一个类的时候,该类是不可靠的!任何在构建中的类都是不可靠的,如果该类已经投入了使用,说明它已通过测试,系统认定它是可靠的,此时如果你重新在编译器中打开该类,试图对它进行修改,那么一个可靠的类将会回到不可靠阶段,这对整个系统的安全性和健壮性是极大的威胁。所以OCP说要对修改封闭,要让投入使用的类不再发生变化。那么如何应对系统的功能扩展呢?以类为单位扩展!这意味着当你写1.0版时,整个系统的架子得允许新类的加入。有了这样的认识,类的合适粒度就有判断依据了。一个类不能做太多事,以至于让它宣称,它已经完整的处理了某件事,这样的类当需求变化时,它便不得不做出修改,从可靠回到不可靠。一个合适粒度的类应该做出这样的宣言:我已经完成了一个小功能,但我并没有完成所有工作,如果你有其他需要,请查询和我类似的其他类。此时软件实体就可以做到OCP——可以扩展类,但不可以修改类。

让人沮丧的是,类的合适粒度永远没有正确答案,重构是编程中一件永远也干不完的事,但从合适的角度思考问题,能让你的系统工作的更舒服,让你的代码看起来更让人赏心悦目。

如何在独自编程的情况下看出层次:

软件的首要使命是:管理软件复杂度。Steve McConnell在那本著名的《代码大全》中使用了大量篇幅讲述这个问题。计算机先驱Edsger Dijkstra这样描述:“在语义的层次量上相比,一般的数学理论几乎是平坦的。由于提出了对很深的概念层次的需要,自动化的计算机使我们面临一种本质上全新的智力挑战。”由于软件逻辑层次太深,现代计算机语言一路从打孔纸带发展到了面向对象编程语言,不同解决方案的提出,本质上就是为了解决一个问题——管理软件复杂度。

新兴的面向对象编程语言其最大的优势就是有效的封装了复杂。然而在一个软件新手面前,面向对象和面向过程几乎没有区别,你可以想象很多刚刚接触编程的人都在使用面向对象语言编写面向过程的代码。这也难怪,学校里老师布置的作业是固定的,需求不会更改,学生只需要写出代码,让计算机得出他们预期的结果,就算完事了。而在真实的软件编程中,需求总会发生变化,软件总是会调整自己的功能,以适应客户的痛点,还有一点不容忽视,那就是代码量。小程序的代码量一个程序员可能不出三天就能完成,这样的程序显然没有必要花费好几个小时的时间去设计层次和模块,加之需求不变,这么做简直是牛刀杀鸡。在miniqueue初期,我也遇到了类似的麻烦,在我的经验内,这样庞大的系统我还没有接触过,曾写过的小程序,小到5个类就能解决问题,如果你不嫌难看完全可以用一个类吞下所有代码。

然而miniqueue不是一个课程设计式的小作业。到目前为止,它的代码量就已经超出了一个人的理解范围——即,你必须依靠类的封装性帮你安全的忽视系统的其他部分。有一个问题浮现了,在团队协作的情况下,人们天然需要设计接口,以实现系统不同部分的衔接,即系统各个部分可以分别同时开发,只要大家事先约定接口。但我是独自编程,整个系统是一部分一部分完成的,不存在同时开发的问题,对于各个类的作用我很清楚,不同的类如何协作我也明白,此时层次就显得异常难以察觉。如同完成一幅巨型壁画,如果是多人配合,你发现不同的人可以被分配以完成不同的层次,如有人画背景,有人画人物。但如果整幅画都由你一人完成,你很可能会从这幅画的某个角落开始画起,而不是先背景后人物这样分层实现。miniqueue的构建过程中我就遇到了这样的情况,它蒙蔽了我的双眼,使得我花了很长一段时间才看出整个系统的层次性。

事后我做了总结,如何在独自编程的情况下,看出系统的层次(当然这个问题只对新人有用,因为你一旦经历过一次,这样的问题就不会再出现,因为你的思考角度已经发生了转变)?

首先,在编程之前,想象两个人物,一是你(类的编写者),一是客户(类的使用者),当然在独自编程的情况下,客户很可能就是3天后的你,但你要认为客户对该类的内部浑然不知,他只能看到这个类的接口,仅此而已。这样一来你就得认真编写类的使用说明,认真编写接口。而当三天后你开始使用这个类的时候,不要去想类的内部是如何实现的,仅仅盯着类的接口,如果你发现类的接口让你摸不着头脑,说明这个类的抽象是存在问题的,这时候不要使用你对它内部的知识来强行使用该类!你应该放下手头的工作,回到那个类的内部,看看是什么原因导致了接口的模糊性!这是看出层次性的第一步,即,你要将类的编写和类的使用完全分离,避免穿过类的接口去使用类。

其次,我要告诉大家一个浅显但却非常容易被忽视的事实——从接口的角度看,系统由底层到顶层是逐渐收紧的!这是一个摆在你面前你马上就会想明白的道理,但实际构建系统的过程中,特别是新手,很容易对它视而不见。表现在,既然这个类,顶层需要使用,为什么不让顶层直接去调用它呢?如果你让顶层直接调用一个非常灵活的类(如同我的界面层直接调用数据库操作层),最后你会发现,实际上你不过是把很多事情压缩到了顶层去做,而顶层最后会被这些不应该属于这个层次处理的事情填的异常臃肿。而一个健康的系统,其底层应该提供数量众多,且灵活的接口,从这里开始,慢慢向上,每个层处理一些事情,并告诉其上层,不用再关注它所处理过的事情,这样一层层向上,很复杂的一个事物将会被层次,分别一点点隐藏起来。当你看到一个灵活的类时,你就应该知道它在层级结构中应该是处于下层的,而当你看到一个接口比较狭窄的类时,你就应该知道它应该处在上层。

最后,实现分层的重要一点——观察接口的抽象!当你在设计一个类的时候,不要写任何代码,相反,你应该敲下一些文字,用以指导你的抽象。一个类是什么,不是由它的名字,或者你对它写的注释决定的,而是它的接口所表现出来的抽象。直立行走,杂食,不能飞翔,体力极好可以连续奔跑40公里,体长175厘米,体重70千克,智商100,对于这个对象,就算我给它命名animal,你也能看出来他是一个human。当你在编写程序的时候,优先考虑的是类的抽象,而不是内部的具体实现,你就能够看出分层的端倪了。因为“层次”说的就是某些抽象一致的类的集合。有了类的抽象,再思考层的抽象,就会清晰许多。不管是类还是层,它们都是为了隐藏某些复杂性而存在的。

代码是如何生长的:

我是个编程新手,又恰巧在这样的情况下,试图去编写一个不简单的程序,导致我把,一段代码从“萌芽”到“破土而出”走了一遍。我想不是任何人都有这个机会,毕竟参加工作后大家面对的是软件架构师已经搭建好架子的程序,多数人并不需要自己重头去架构一个程序,何况是在经验匮乏的情况下去架构一个程序。这一路走来,我推翻了很多设计,重写了很多代码,它们成为了回收站里的牺牲品,但在我的脑海里,它们为我构建了一个全新的编程世界。总结来说,永远不要有一劳永逸的想法,代码从来不是固定的,它一直都在生长——何为生长?——在迭代中精进与断舍离。

展开阅读全文
OCP
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部