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