解决问题和设计
“自顶向下”(分而治之)
1、辨识你的核心问题(例如:我想构建一个能做X的应用程序编程接口API)。
2、将已辨识的问题分解为逻辑部分(例如:我需要一个网络服务器,一个数据库,以及一些端点)。
3、将以上问题更深入地分解为细致的逻辑单元,思考“这个单元包括什么东西?”、“这个单元怎么工作”,“为什么我需要它”等等。
4、反复重复第三步,直到留下一些小问题,而对怎么去处理它们你有非常清楚的概念。
上面的例子显示了我们该如何开始分解想法去构建一个应用,这个应用能帮助潜在的动物领养人在我们的动物收养中心去找到他们心仪的宠物。
第一件事情是,辨识涉及到用户(宠物将来的主人)和管理者(那些运营收养中心的好心人)的动作行为。从这里开始,我们分解每个步骤成为一些技术性的要求。我们一直进行这样的分解动作直至最终留下了一些问题,这些问题我们可能能够很快开始编码去解决。
执行自顶向下的分析动作可能也是你的构建系统中面对主要“利益相关者(客户)”时处理的事情。现在你需要做的是扫描你已经辨识到的所有需求,问题,以及潜在的解决方案,努力提出你需要去构建的一系列事情。
交流你的设计
在这个例子中,有一个类似下面步骤的列表:
1、用户和管理者注册和会话处理;
2、动物信息管理;
3、用户喜好管理;
4、动物查询和动物主人匹配;
5、预约调度。
我们可以将这个列表分离开,分配一个或多个子项目到小的开发团队。现在我们已经大体知道我们需要构建什么内容,是时候去确定我们怎样去构建它,或者,更精确地说,最终产品应该怎么运行。
方盒箭头表示的流程图
这里的思路是,努力绘制系统组件之间的数据流。在一个高的层次上描绘系统,可以让我可视化那些组件,能够立即开始规划如何去叙述和构建每个组件。
上图是一个应用的架构概览。可以看到那些需要我们去构建的主要的子系统,以及它们之间和用户之间是如何交互的。每个灰色方块是能够独立实现的子系统,当构建的时候可以连接到其他子系统,这种图表的缺点是太抽象了。
用户故事
用户故事是些简短的,一段一段的故事,关于用户与系统之间交互来达到某个目标时采取的步骤。通常情况下,你会对前置条件做些假设,例如,假定用户已经登录了。下面是用于描述预约调度的行为的例子:
(1)登录后,用户点击“上一次查看的动物”;
(2)用户从他们最近查看的动物文件列表中选择一种动物;
(3)用户点击“安排一个预约”;
(4)用户从日历中选择一个时间点,点击“提交”。
如果你需要处理一些有条件的实例,你可以在步骤下创建一个子列表。用户故事的优势是,它能全面描述一个完整特性是如何工作或者一个要求是怎样被满足的。它提供了一些你能显式测试的例子,可以帮你去验证一个特性是实实在在存在的。缺点是,对每个功能的关键点写用户故事有很多的工作量,依赖于你想得多细致。很难证明通过这样一种方式你已经覆盖了所有东西,然后你可以去有意图地开始分配工作。
实现
现在已获取了系统的所有需求,并将每个需要解决的问题分解成了小块工作,你准备开始构建软件。
文档
当我们回看方盒箭头(或架构)图表时,我们经常关注里面的规模大的模型。我们这么做是因为它们代表了这些东西:组件、我们的数据库、用户等等。还有这些模型之间的箭头,它们告诉我们什么组件需要与其它组件进行交互,以及两个组件之间是如何交互的信息,不管是显式还是隐式地提示出来。这就是文档的来源。
文档类型
1、关系图:用来可视化数据库中数据是如何建模的;
2、架构图:显示一部分软件的子系统,以及它们之间是如何作用联系的;
3、用户故事:解释了用户如何完成一个任务;
4、API规格说明:描述了服务/模型/对象/…暴露给用户的方法,它们期待什么输入,产生什么输出。
5、代码注释:当考虑功能中某部分的实现时给出的注释,主要是方便别的开发者和你自己将来定位代码。
文档用于解释为何一些东西使用一种特别的方式运行,以及它是怎么运行的。给定的文档应该叙述多少东西严格取决于内容。往API规格说明中拷贝函数符号是比较合适的。最理想的是,你应该在构建东西或写测试代码之前输出文档。
1、你能够提交文档给你的同事或领导以便评审从而能够获取有价值的反馈意见;
2、当同事有可用文档时,可以使他们方便开始自己负责的编写相关文档和编译子系统的工作;
3、文档代表了测试和实现之间的一份合约。
测试
代码需要有测试伴随。
文档驱动的测试先行开发
文档代表了测试和实现之间的一份合约,是因为文档充当了一个非常合适的顶梁柱,每件事情都能依赖它。一个关于TDD(测试驱动开发)非常普遍的抱怨是,在实现任何东西之前,很难知道要测试什么,而实现却又趋向于驱动代码一级的大部分设计和决策。你的文档需要足够完善,使得你能够简单地编写测试代码去验证实现是否满足了文档中描述的那些条件和接口。建立这个之后,测试就能够告诉代码是否运行成功,是否执行了文档中叙述的情况。
测试也提供了另外一个对你的代码库有价值的特性,一个支点。当你需要修改实现的时候,不管你是添加一个新特性还是修改某事物的工作模式,你应该再一次开始更新文档,保证它是最新状态。做完之后,你能够去重构或添加新测试代码去检验你的代码符合新的规范。即使你的实现未能通过测试,失败的案例也能够作为修改实现的一个指导。
自底向上的开发
自顶向下开发方法会失败,因为它假设你在还未开始构建之前就清楚最终构建的东西是什么样子的。但是即使有个正式的规格说明书,这也是非常罕见的。当出现改变或过去问题有了新的解决方案时,很难转换你的对象层级,也难以往中间层插入对象。而且,以一种“首先抽象,最后授权细节”的方式开发的软件经常是很复杂的。
在自底向上方法中,你开始编码去解决系统中需要处理的最低级的操作。在动物收养例子中,这些操作可能是SQL查询数据库操作,数据库用于管理动物信息、用户信息、喜好信息、预约信息。
接下来就是要构建一系列的抽象集合,像是你在编程语言中使用的函数,更低级操作的调用,清除操作,输入输出结构。从这里开始构建更高级的抽象,例如模型类,然后调用你编写的模型方法构建API端点。
将低级操作汇总成高级操作的行为通常被称为构造(composition)。
自底向上方法中需要注意的一个关键点是,当你的目标只是创建尽可能多的抽象层,而每一层只会调用到它下面的低级层中定义的功能时,你有很多自由度去构造功能。鉴于层之间的交叉冲突会导致混乱的代码,我认为,这个方法无论如何也会给你提供相当大的灵活性。当你要改变某些东西时,你可以很方便地添加有用的功能到你正在工作的层以下,以便构建一个适当抽象的解决方案。
测试时,自底向上方法也很容易去处理。自顶向下方法经常需要使用一些技巧像是在更高级抽象层用仿真来模拟低级层的实现,而自底向上方法允许你在进入抽象之前测试加固你的低级层代码。这就意味着你能有效地在每一抽象层进行单元测试,使用集成测试去检验你的构造函数是否如预期一般。如果你以一种测试优先的风格进行开发,或者至少全面测试你构建的每一层,你也会很有信心说,你构建的下一层就如你用来构造低层级功能的代码一样有同样少的错误。
流程:
1.与客户交谈,获取有关特性和需求的尽可能多的信息。
2.将每个特性和需求分解成子任务,直到你有了可辨识、可执行的动作块。
3.确定和文档化你要构建的系统中的组件来解决你之前概述的问题。
4.文档化你的设计,解释每个需要实现的组件、接口。写用户故事帮助理解细节。
5.从其他开发者和客户那里获取反馈,确保你已经掌握了每件需要做的事情。
6.在对上述文档做了必要改动之后,建立你的开发环境,开始创建低级功能的测试用例。
7.在实现了系统中最低级的工作单元之后,为抽象层编写测试用例并实现它们。
8.重复5-7步骤直到最后完成。在其中你可以引入其他你们团队觉得合适的开发方法。