在我的上篇文章中,对软件工匠精神进行了整体介绍。运用敏捷设计是践行工匠精神的重要组成,本文将以敏捷设计为主题,介绍Why、What,并以实际工作中的前端代码为例,阐述How。
软件设计
在按照我的理解方式审查了软件开发的生命周期后,我得出一个结论:
实际上满足工程设计标准的唯一软件文档,就是源代码清单。
—— Jack Reeves, 《什么是软件设计?》1992年发表于《C++ Journal》
在随后的内容中,我会经常谈到“设计”。
你不应认为设计就是一组没有代码的UML图,尽管UML描绘了设计的一部分,但它不是设计。软件的设计是个抽象概念,它和程序的形态、结构以及每个模块、类、方法的详细形态和结构相关。尽管你可以使用很多载体去描绘,但设计最终体现为源代码。
源代码即设计!
软件设计腐化
接下来,将时间跨度放大,你将看到软件设计腐化的典型过程。
最初,在项目启动时,你对系统的设计已经了然于胸,保持这个图像的清晰至关重要。或许到了第一次版本发布时,设计依然清晰。
随后,事情开始变糟,系统就像面包一样开始腐化。随着时间的流逝,腐化会蔓延、增长,丑陋腐烂的疖子在代码中积累,使它变得越来越难维护。
最终,即便是最简单的更改,也需要花费巨大的工作量。开发人员和一线管理者开始强烈要求重新设计!然而,这样的重新设计很少会成功,设计人员会发现他们正朝着一个移动的目标开炮。老系统不断发展、变化,新的设计必须跟上这些变化。
于是,新的设计还没发布,就又会积累许多瑕疵和弊病。
为了能判断软件是否正在腐化,我们需要灵敏的嗅觉,嗅出腐化的气味。
当软件出现下面任何一种气味时,就表明它正在腐化。
僵化的臭味:
很难对系统进行改动,因为牵一发而动全身,每个改动会迫使对系统其他部分进行改动。
脆弱的臭味:
对系统的改动会导致系统中与改动处概念无关的许多地方出现问题。
牵绊的臭味:
很难对系统抽丝剥茧,难以剥离出可被其他系统复用的组件。
粘滞的臭味:
做正确的事情比做错误的事情困难。
非必要复杂的臭味:
设计中某些基础结构不具备任何直接益处。
非必要重复的臭味:
设计中存在重复结构,原本可以通过抽象进行统一。
晦涩的臭味:
代码难以阅读和理解,没有很好地表达意图。
软件为什么会腐化呢?
需求经常以初始设计没有预见到的方式变化,这导致了设计的退化。改动往往迫于业务或进度压力,而改动人员对原始的设计思路不熟悉。虽然改动后可以工作,但却违反了原始的设计。改动越来越多,违反逐渐积累,臭味开始出现。但我们不能怪需求变化。每个人都知道需求是项目中最不稳定的因素,如果设计因为需求不断变化而失败,就表明我们的设计本身是有缺陷的。
所以我们必须要设法找到一种方法,使得设计对于这种变化具有灵活性,并采取一些实践来防止设计腐化。
像敏捷团队一样设计
敏捷团队依靠变化获取活力,团队懂得“由俭入奢易,由奢入俭难”,所以几乎不会进行提前设计,也不要求有成熟的初始设计。他们愿意保持设计尽可能干净、简单,并使用许多单元测试和验收测试编织成安全网。为了保持设计的灵活性和易理解性,敏捷团队会持续改进设计,从而使得每次迭代结束所生成的系统都具有最适合那次迭代的设计。
敏捷开发人员应承诺保持设计尽可能适当、干净。这可不是随便或敷衍的承诺,因为他们不是每几周才清理设计,而是每天、每小时、每分钟都要保持软件尽可能干净、简单、富有表现力。他们绝不接受“一会儿我再把它改好”,这和医生对待消毒的态度是一样的。专业的软件开发人员绝不接受代码腐化。
所以,敏捷设计并不是一件事,而是一个过程,它要求持续遵循与运用软件设计原则和模式来改进软件的结构和可读性,保持系统设计在任何时间都尽可能简单、干净和富有表现力。
随后,我们将会用实际工作中的代码来示范如何运用这些原则与模式。
软件设计原则的运用
现在,我们要实现“在流水线列表中,显示每条流水线最后一次运行的执行用时”这样一个功能。
这个功能的具体逻辑如下:
如果 |
当 |
那么 |
一条流水线正在运行 |
时间前进到下一秒 |
更新显示最新的用时 |
一条流水线已经结束运行 |
不更新,显示固定用时 |
|
一天流水线从运行转为结束 |
更新显示最新的用时 |
我们先来简要说明一下我们即将遵循的原则,然后看看如何将这些原则运用在这个功能中。
软件设计五大原则:
职能单一原则(SRP):
一个类应该仅有一个引起它变化的原因(即职责)。一个类承担的职能太多,就会导致一个职能的变化影响它完成其他职能的能力,这种脆弱的设计很容易在发生变化时产这意料之外的破坏。SRP是所有原则中最简单的一个,也是最难正确运用的一个。下面论述的其他原则都会以某种方式回到这个问题上。可以说,软件设计真正要做的大部分内容就是识别职能并对这些职能进行分离。
开放关闭原则(OCP):
实体(类、模块、函数等)应该是可扩展的,但是不可修改的。如果程序中的一处改动会导致一系列连锁改动,这就是僵化的臭味。OCP的关键在于对程序中频繁变化的那些部分进行抽象。当模块依赖于固定的抽象体,它对于修改就是关闭的;通过对抽象体进行派生和实现,就可以扩展这个模块的行为。
里氏替换原则(LSP):
子类型可以替换父类型。LSP是使OCP成为可能的一大主要原因,子类型的可替换性使得基类型模块在无需修改的情况下就可以实现扩展。
依赖倒置原则(DIP):
高层模块不应依赖低层模块,二者应该依赖于抽象;抽象不应依赖于细节,细节应依赖于抽象。当高层模块依赖低层模块,低层模块的修改就会直接影响到高层模块,迫使做出一连串的改动。DIP要求包含高层业务规则的模块应当优先并独立于包含实现细节的模块,这样高层模块就可以非常容易被复用,这是框架设计的核心原则。同时,由于抽象和细节彼此隔离,所以能够轻松构建出面对变化富有弹性的、易于维护的代码。
接口隔离原则(ISP):
不应强迫调用端依赖于它们不用的方法。如果一个类包含了客户程序不需要的方法,那么当其他客户程序要求对这个类进行改动时,就会对这个客户程序产生影响。遵循ISP,你需要将这种胖类拆分成为不同客户程序特定的接口,然后用胖类集成这些接口,从而解除客户程序和它们没有调用的方法之间的依赖关系,并且做到客户程序之间互不依赖。
回到要做的定时更新功能,看看如何运用。
第一步:识别职能并运用SRP
我们首先要做的是识别出需求中的职能。我们发现,这个功能涉及到三个职能:
职能1:显示用时
职能2:计算用时
职能3:定时
接下来,我们运用SRP,创建三个前端组件。
组件 |
职能 |
逻辑 |
AutoRefresh.vue |
定时 |
定时重新计算并显示用时 |
ElapsedTime.vue |
计算 |
计算并显示用时 |
RunElapsedTime.vue |
显示 |
显示用时 |
第二步:梳理职能间的依赖并运用DIP
如果按照上面的面向过程的方式,三个职能之间的依赖关系应当像下图一样:AutoRefresh调用并依赖 ElapsedTime,ElapsedTime 调用并依赖 RunElapsedTime。
为了避免显示的改变迫使计算改变、计算的改变迫使定时改变,我们需要遵循DIP。
1、倒置计算和定时之间的依赖
为了让定时不依赖于计算组件,我们需要对定时触发的内容进行抽象。我们希望,定时触发的可以是重新计算时间,也可以是计算别的什么,具体定时触发的是什么,在定时组件中并不关注。
我们在AutoRefresh组件中运用Slot来实现这种倒置,因为Slot可以认为是一种抽象,我们要让AutoRefresh依赖于抽象,而不关注细节。
如下图所示:
同时,Slot中的具体实现应当满足某种约束,以便具体的计算能够被AutoRefresh触发。
此处,我们要求由计算组件告诉AutoRefresh组件触发计算的方法指针。
为此,我们为AutoRefresh组件添加handler属性,在AutoRefresh中对handler加以调用,由具体的计算组件传入handler的实现。
如下图所示:
这样,我们的AutoRefresh已经具备了足够的灵活性,可以在不修改AutoRefresh组件代码的情况下,对定时计算场景进行扩展(即,在遵循DIP的同时,做到了OCP)。
2、倒置显示和计算之间的依赖
同样的方式,我们希望时间计算组件ElapsedTime不依赖于具体的展现形式,以便在不同的时间显示场景中都无需修改计算逻辑代码。
我们首先利用Slot对显示组件进行抽象,同时将计算结果(也就是用时数据elapsed)传递给低层组件。
如下图:
同时,elapsed计算需要依赖于开始和结束时间,
具体计算逻辑如下:
状态 |
开始时间 |
结束时间 |
用时计算 |
进行中 |
有 |
无 |
当前时间-开始时间 |
已完成 |
有 |
有 |
结束时间-开始时间 |
也就是说,我们需要让ElapsedTime组件接收显示组件输入的开始和结束时间数据。
为此,我们需要在ElapsedTime增加startedAt和stoppedAt两个属性,并由显示组件传入。
为了能够接收来自ElapsedTime组件的计算结果,低层显示组件需要按上面的接口契约输入startedAt、stoppedAt,并获取elapsed数据。
如下图:
这样,我们就可以在不修改ElapsedTime代码的情况下,通过扩展新的显示组件来实现在需要的时间显示了。
总结
在软件设计中,我经常听到“高内聚、低耦合”、“做成可扩展的”、“做成可复用的”,然而,我却发现,绝大多数人根本不知道怎样才能有效实现这些目标,甚至完全误解了这些话的含义。
在践行软件工匠精神的道路上,你需要的不是说一说、不是凭直觉、不是凭习惯和喜好就能设计出优秀的软件;你需要的是刻意练习、刻意让自己遵循设计原则、在具体上下文中利用原则进行分析、利用模式解决问题。
每个人都有自己写代码的风格并非理所当然,代码的不同设计必然有优劣,你需要随时关注设计中的臭味,养成代码洁癖,并为此做出郑重的、要长期坚持的承诺。
作者:李淳
欢迎关注
微信公众号 EBCloud
原创文章,作者:EBCloud,如若转载,请注明出处:https://www.sudun.com/ask/33492.html