1.引言
“Hello,wolrd!(世界,你好!)”,我写一些技术性的文章已经有一段时间了,最近一段闲暇时间我没有休息,而是思考我可以写一些东西,可以对朋友们提升开发技能提供一些可能的帮助。自从来到 Stratum Security 公司工作后,我已经从无到有建立了几个新系统并做了好些设计、文档、重构工作。我认识到将这些事情记录到一处可能有些用处,能够帮助减少为了获取这些概念去阅读不同文章的压力。
2.目标人群
我一直想要分享一些经验,分享这些年从编程和最近工作中学到的经验,但是在博客中我不想去深入到编程的细节。我宁愿写写短小,精炼的文章,能够给读者灌输一些领悟,这些领悟来自于我构建系统的经验。因此,这篇文章针对于我所谓的“中级”开发人员,你可能会感悟最深。中级开发人员是这样一种人,他已经对编码过程相当熟悉了,开始思考一些抽象东西像是如何编写软件使得它在生产上是可靠的,既能够灵活变通,也能让同事理解的代码。
你将要读到的东西是有些偏向的,因为它们是根据我个人喜好而未考虑类似编程范式的东西,我拒绝盲目接受教条的流程和方法。我这里诉说的东西受到了工厂最佳实践,包括那些工作中有名的优选的方法、技术,更加受我自身经验的影响。当然了,我觉得对我的文章你要秉持怀疑态度。如果你是面向对象的狂热分子,你最好还是现在就离开此网页。
3.解决问题和设计
假定你已经编程有一段时间了,你可能内心已经清楚意识到从有一个关于写东西的点子直接跳到开始写编码是一个非常不好的做事方式。
上面的流程在实际生活中不可能生效。至少在你写一些代码给同事看的时候不会发生。因而,我们要去探寻的是怎样去获取“一些神奇事件”的部分,将它拆解—彩虹,独角兽,等等—直到我们拥有了专业的方法去确定要构建什么以及怎么去构建它,这样我们就能和团队成员沟通,也能减少未来让我们犯头痛的次数。我保证最后那些东西都是有趣和有意思的。我们的目标并不是将软件开发归结为一个完美的科学(我确信如果你这样做一定会失败的),而是辨析一些能指引我们通过混乱迷雾的策略。
3.1.自顶向下的方法
在涉及到解决问题类似的事情时,这句话你可能已经听到过多次,那就是“自顶向下”或者可能是“分而治之”。后一个名称更加有价值,正如它所建议的,这是一个相当简单的解决问题的策略,它包含以下几步:
- 辨识你的核心问题(例如:我想构建一个能做X的API)。
- 将已辨识的问题分解为逻辑部分(例如:我需要一个网络服务器,一个数据库,以及一些端点)。
- 将以上问题更深入的分解为细致的逻辑单元,提些问题类似“这个单元包括什么东西?”、“这个单元怎么工作”,“为什么我需要它”,等等。
- 反复重复第三步,直到留下一些小问题,而对怎么去处理它们你有非常清楚的概念。
上面的例子显示了我们该如何开始分解想法去构建一个应用,这个应用能帮助潜在的动物领养人在我们的动物收养中心去找到他们心仪的宠物。第一件事情是,辨识涉及到用户(宠物将来的主人)和管理者(那些运营收养中心的好心人)的动作行为。从这里开始,我们分解每个步骤成为一些技术性的要求。我们一直进行这样的分解动作直至最终留下了一些问题,这些问题我们可能能够很快开始编码去解决。
执行那个我们刚刚做的自顶向下的分析动作可能也是你的构建系统中面对主要“利益相关者(客户)”时处理的事情。这个词是设想的敏捷词语,用来描述这一类人,要你把给他们构建的东西放在第一位,或者是一些代表你未来用户的人。然而,这可能不是你想要给开发者团队带去的东西,这些开发者要你去分配任务。实际上,你的问题没有一个能够分解成如此清晰的层次结构,接下来我们会看到这是件好事情!现在你需要做的是扫描你已经辨识到的所有需求,问题,以及潜在的解决方案,努力提出你需要去构建的一系列事情,这里使用了一些相当技术性的行话。
3.2.交流你的设计
在那个宠物收养的例子中,可以想象到我们会有一个类似下面步骤的列表:
- 用户和管理者注册和会话处理;
- 动物信息管理;
- 用户喜好管理;
- 动物查询和动物-主人匹配;
- 预约调度。
这些个是有些厚重的,相对独特的高级别技术问题,这些问题我们的应用一定会遇到。我们可以想像着将这个列表分离开,分配一个或多个子项目到小的开发团队。现在我们已经大体知道我们需要构建什么内容,是时候去确定我们怎样去构建它,或者,更精确地说,最终产品应该怎么运行。
方盒箭头表示的流程图
这是另一个很技术性的术语,描述了上面我们创建的那种图表。不是去努力分解问题成子问题,这里的思路是,我们努力绘制系统组件之间的数据流。这正是我喜欢的方式,在一个高的层次上描绘系统,因为它可以让我可视化那些组件,能够立即开始规划如何去叙述和构建每个组件。
上图是我们过去设计的一个应用的架构概览。你能看到那些需要我们去构建的主要的子系统,以及它们之间和用户之间是如何交互的。每个灰色方块是能够独立实现的子系统,当构建的时候可以连接到其他子系统。稍后将详细介绍。
这种图表不好的地方在于,他们太抽象了,有时候努力分解系统成为类似这样的组件会弊大于利。例如,如果你在构建一个REST API,用这种方法你可能不会收获很多。
用户故事
另一个我要描述的抽象交流方法就是在软件工程课程中经常被教授的方法。但是它可能并未在编程(自学)教育指定的书籍中出现。用户故事正如字面上所显示的含义。它们是些简短的,一段一段的故事,关于用户与我们的系统之间交互来达到某个目标时采取的步骤。通常情况下,你会对前置条件做些假设,例如,假定用户已经登录了。
这里是一个用户故事的例子,用于描述上面那个动物收养例子中预约调度的行为。
- 登录后,用户点击“上一次查看的动物”;
- 用户从他们最近查看的动物文件列表中选择一中动物;
- 用户点击“安排一个预约”;
- 用户从日历中选择一个时间点,点击“提交”。
就是那么简单!当然了,如果你需要处理一些有条件的实例,你可以在步骤下创建一个子列表。用户故事具有如下优势,它能全面描述一个完整特性是如何工作或者一个要求是怎样被满足的。它提供了一些你能显式测试的例子,可以帮你去验证一个特性是实实在在存在的。不好的地方在于,对每个功能的关键点写用户故事有很多的工作量,依赖于你想多细致。很难证明通过这样一种方式你已经覆盖了所有东西,然后你可以去有意图地开始分配工作。
4.实现
现在你已经获取了系统的所有需求,充满希望地将每个你需要解决的问题分解成了小块工作,你准备开始构建软件。但是,打住!现实世界中构建软件不仅仅是蹲下来写代码,就如同建房子不仅仅是将一块一块砖叠放到另一块上面!
4.1.文档
当我们回看方盒箭头(或架构)图表时,我们经常关注里面的规模大的模型。我们这么做是因为它们代表了这些东西:组件、我们的数据库、用户,等等。还有这些模型之间的箭头,如果图中没有更重要东西!箭头出现在那里不是用来显示的,它们告诉我们什么组件需要与其它组件进行交互,更会包括两个组件之间是如何交互的信息,不管是显式还是隐式的提示出来。这就是文档的来源。
文档类型
- 关系图 用来可视化数据库中数据是如何建模的;
- 架构图 显示一部分软件的子系统,以及它们之间是如何作用联系的;
- 用户故事 解释了用户如何完成一个任务;
- API规格说明 描述了服务/模型/对象/…暴露给用户的方法,它们期待什么输入,产生什么输出。
- 代码注释 当考虑功能中某部分的实现时给出的注释,主要是方便别的开发者和你自己将来定位代码。
当你编码时,这里仅仅是需要编写的文档中的一少部分。这里的中心主题是,文档用来与其他开发者和将来的你交流编码意图。换言之,文档用于解释为何一些东西使用一种特别的方式运行,以及它是怎么运行的。给定的文档应该叙述多少东西严格取决于内容。可能你不需要在代码注释中重写你的用户故事,在用户故事中也不需要拷贝函数符号。然而,往API规格说明中拷贝函数符号是比较合适的。
最理想的是,你应该在构建东西或写测试代码之前输出文档。这并不是说你要提前写完所有文档,但是你应该把那些你实际构建之前需要准备的文档写完。
- 你能够提交文档给你的同事或领导以便评审从而能够获取有价值的反馈意见;
- 当同事有可用文档时,可以使他们方便开始自己负责的编写相关文档和编译子系统的工作;
- 文档代表了测试和实现之间的一份合约,这点待会儿还要细说。
4.2 测试
虽然对测试驱动开发以及相关细节持怀疑态度,开发者社区对另一个观点看上去已经取得了一致,即代码需要有测试伴随。我个人也赞同测试先行的开发策略。
文档驱动的测试先行开发
我上面说的话:“文档代表了测试和实现之间的一份合约”,是因为文档充当了一个非常合适的顶梁柱,每件事情都能依赖它。一个关于TDD非常普遍的抱怨(这点我也有深有同感)是,在实现任何东西之前,很难知道要测试什么,而实现却又趋向于驱动代码一级的大部分设计和决策。你的文档需要足够完善,使得你能够简单地编写测试代码去验证实现是否满足了文档中描述的那些条件和接口。建立这个之后,你应该会感到自信满满,因为测试能够告诉代码是否运行成功,是否它执行了你文档中叙述的情况。
测试也提供了另外一个对你的代码库有价值的特性,一个支点。当你需要修改实现的时候,不管你是添加一个新特性还是修改某事物的工作模式,你应该再一次开始更新文档,保证它是最新状态。做完之后,你能够去重构或添加新测试代码去检验你的代码符合新的规范。即使你的实现未能通过测试,失败的案例也能够作为修改实现的一个指导。你也能修改实现而不至引起混乱。
4.3自底向上的开发
采用自顶向下方式去解决问题是一个非常有效的法子。因为我们最开始就清楚我们的工作要在哪里结束,所以我们能将最终目标分解成一块一块直到获取到今日我们能解决的问题。许多人在开发中努力应用自顶向下的方法,特别是那些很喜欢OPP,或面向对象编程的家伙。正如我在博客开头所说的,我对OOP是有严重偏见的,而且我认为自顶向下开发不如自底向上开发有效。自顶向下开发方法会失败,因为它假设你清楚最终构建的东西是什么样子的,在你还未开始构建之前就清楚。从我和许多其他人的经验看来,即使有个正式的规格说明书,这也是非常罕见的。当出现改变或过去问题有了新的解决方案时,很难转换你的对象层级也难以往中间层插入对象。而且,以一种“首先抽象,最后授权细节”的方式开发的软件经常是很复杂的(是ObjectFactoryFactories模式吧,有谁清楚?)
针对自底向上开发的一个常见争论是,它是一种无结构,无组织的,你会写一些最终会扔掉的代码。事实上,自底向上开发正好是无结构无组织的反面,另外,删掉代码也是好事。
在自底向上方法中,你开始编码去解决系统中需要处理的最低级的操作。在那个动物收养例子中,这些操作可能是SQL查询数据库操作,数据库用于管理动物信息、用户信息、喜好信息、预约信息。接下来就是要构建一系列的抽象集合,像是你在编程语言中使用的函数,更低级操作的调用,清除操作,输入输出结构。从这里开始构建更高级的抽象,例如模型类,然后调用你编写的模型方法构建API端点。
将低级操作汇总成高级操作的行为通常被称为构造(composition)。正如数学中你学到的如下两个函数是如何构造的一样,
(f.g)(x) = f(g(x))
软件中构造也是一样的原则运行。当然了,既然我们是讨论代码,我们在其中会做很多额外的工作,但是核心思想是一样的。自底向上方法中需要注意的一个关键点是,当你的目标只是创建尽可能多的抽象层,而每一层只会调用到它下面的低级层中定义的功能时,你有很多自由度去构造功能,只要你认为合适的方式都行。鉴于层之间的交叉冲突会导致混乱的代码,我认为,这个方法无论如何也会给你提供相当大的灵活性。当你要改变某些东西时,你可以很方便地添加有用的功能到你正在工作的层以下,以便构建一个适当抽象的解决方案。
测试时,自底向上方法也很容易去处理。自顶向下方法经常需要使用一些技巧像是在更高级抽象层用仿真来模拟低级层的实现,而自底向上方法允许你在进入抽象之前测试加固你的低级层代码。这就意味着你能有效地在每一抽象层进行单元测试,使用集成测试去检验你的构造函数是否如预期一般。如果你以一种测试优先的风格进行开发,或者至少全面测试你构建的每一层,你也会很有信心说,你构建的下一层就如你用来构造低层级功能的代码一样有同样少的错误。
5.结语
下面几点将我之前叙述的内容总结成一个流程,在你的下一个软件项目中你可以直接使用此流程。
- 与你的客户(利益相关者)交谈,尽力去获取有关特性和需求的尽可能多的信息。
- 将每个特性和需求分解成子任务,子子任务直到你有了可辨识、可执行的动作块。
- 确定和文档化你要构建的系统中的组件来解决你之前概述的问题。
- 文档化你的设计,解释每个需要实现的组件、接口。写用户故事帮助理解细节。
- 从其他开发者和客户那里获取反馈,确保你已经掌握了每件需要做的事情。
- 在对上述文档做了任何必要改动之后,建立你的开发环境,开始创建低级功能的测试用例。
- 在实现了系统中最低级的工作单元之后,为抽象层编写测试用例并实现它们。
- 重复5-7步骤直到最后完成。在其中你可以引入其他你们团队觉得合适的开发方法。
希望我这篇文章能给你,可能是一个中级开发者,提供一点关于现实世界中软件工程是如何运行的概念。我们已经揭开了之前提到的“神奇事件”,但是我希望你接受这一事实,我们这里讨论的创造性的和社会化的流程一直会是有挑战性的和令人满意的。
最后,将高水平软件工程师与“好的编码者”区分开来的是与团队沟通规划的能力,理解需求并转换成为技术模型的能力,以及严格遵循开发流程的能力。