原文地址:https://dzone.com/refcardz/designing-quality-software?chapter=1
提供一套用于构建更好的软件的设计原则,编码原则,测试原则,环境规则以及通用规则清单。
可以将软件的技术质量定义为软件系统源自常识和最佳实践中的一组原则和规范的一致性级别(译注:强调原则和规则的通用性)。 这些规则应涵盖软件架构(依赖关系结构),日常编程,测试和编码风格。
技术质量从根本上体现在源代码中。 人们说:“真像只能在源代码中找到”。 因此,达到令人满意的技术质量水平是开发过程中的明确目标和不可或缺的部分,这一点很重要。 为了避免在开发过程中技术质量不断下降,需要定期(至少每天)对其进行度量。 通过这样做,有可能在流程的早期检测并解决不良的违反规则的行为。 越晚检测到违反规则,修复它们的难度和成本就越高。 由于测试只是技术质量管理的几个方面之一,因此仅凭测试就不可能达到可接受的技术质量水平。
本文首先描述了技术质量的最大敌人:即软件架构侵蚀。 应对架构侵蚀的最好方法是保持软件系统的大规模结构处于良好的状态。 因此,本文着重于大规模系统设计,这对应用程序安全性方面也具有重要意义。 前半部分的内容非常技术性, 目的是支持架构师和开发人员解决可能对技术质量和软件架构产生负面影响的典型日常问题。 后半部分包含一组从经验和实际项目中得出的精炼规则。 实施和执行这些规则将帮助你达到较高的技术质量和可维护性,同时优化开发团队的生产力。
本文目标受众是软件架构师,开发人员,质量经理和其他技术利益相关者。 尽管文章的主要部分是与编程语言无关的,但最后的规则集最适合与静态类型的面向对象的语言(例如Java,C#或C ++)一起使用。
所有软件项目都始于希望和雄心。架构师和开发人员致力于创建一款易于维护且有趣的优雅,高效的软件。 通常,他们在脑海中具有预先设计的重要想象。 但是,随着代码库的扩大,情况开始发生变化。 该软件越来越难以测试,理解,维护和扩展。 用Robert C. Martin的话来说,“该软件开始像一块烂肉一样腐烂”。
这种现象被称为“架构腐蚀”或“架构债务累积”,并且这种现象几乎发生在每个重要的软件项目中。 通常,由于需求的变化,时间压力或简单的疏忽,腐蚀始于与预先设计的微小偏差。 在项目的早期阶段,这不是问题。 但是在后期阶段,架构债务的增长速度远快于代码库。 作为此过程的结果,将变更作用于系统而不破坏某些东西变得更加困难。 生产力显着下降,变更成本不断增长,直至无法承受。
Robert C. Martin described a couple of well-known symptoms that can help you to figure out whether or not your application is affected by structural erosion:
Robert C. Martin描述了几个众所周知的症状,这些症状可以帮助弄清你的应用程序是否受到架构侵蚀的影响:
你可能会同意这些症状会以一种或另一种方式影响大多数不重要的软件系统。 而且,系统越老,使用它的人越多,症状变得越严重。 避免它们的唯一方法是,在日常开发过程中制定一项抗衡架构侵蚀的斗争计划。
软件系统的大规模设计通过其依赖关系结构得以体现。 只有明确管理整个软件生命周期中的依赖性,才有可能避免架构侵蚀的负面影响。 依赖管理的一个重要方面是避免软件组件之间编译时的循环依赖性:
案例1显示了单元A,B和C之间的循环依赖性。因此,无法为这些单元分配等级编号,从而导致以下不良后果:
情况2代表形成无环有向依赖图的三个单元。 现在可以分配等级编号。 结果如下:
请记住,这是一个非常简单的示例。 许多软件系统都有数百个单元。 拥有的单位越多,能够对依赖关系图进行平衡就变得越重要。 否则,维护它们将成为一场噩梦。
以下是公认的软件架构专家对依赖管理的评价:
“It is the dependency architecture that is degrading, and with it the ability of the software to be maintained.” [ASD]
“The dependencies between packages must not form cycles.” [ASD]
“Guideline: No Cycles between Packages. If a group of packages have cyclic dependencies then they may need to be treated as one larger package in terms of a release unit. This is undesirable because releasing larger packages (or package aggregates) increases the likelihood of affecting something.” [AUP]
“Cyclic physical dependencies among components inhibit understanding, testing and reuse.” [LSD]
依赖性管理的另一个重要目标是最大程度地减少软件不同部分之间的整体耦合。 较低的耦合意味着更高的灵活性,更好的可测试性,更好的可维护性和更好的可理解性。 此外,较低的耦合度还意味着变更只会影响应用程序的较小部分,从而大大降低了回归错误的可能性。
为了控制耦合,有必要对其进行测量。 [LSD]描述了两个有用的耦合度量: 平均组件依赖关系(Average Component Dependency,ACD) 告诉我们随机选择的组件将依赖于平均值(包括自身)有多少个组件。 标准化累积组件相关性 (Normalized Cumulative Component Dependency,NCCD)正在将依赖图(应用程序)的耦合与平衡二叉树的耦合进行比较。
在上面,您看到两个依赖关系图。 组件内部的数字反映了可从给定组件(包括自身)访问的组件数量。 该值称为组件依赖关系(CD)。 如果将图1中的所有数字相加,则总和为23。此值称为“累积组件相关性”(CCD)。 如果将CCD除以图中的组件数,则将得到ACD。 对于图1,该值为3.29(23/7)。
请注意,图1包含循环依赖性。 在图2中,删除红色所示的相关性破坏了循环,从而将CCD减小到19,将ACD减小到2.71(19/7)。 如您所见,打破循环肯定有助于实现我们的第二个目标,即从整体上减少耦合。
NCCD is calculated by dividing the CCD value of a dependency graph through the CCD value of a balanced binary tree with the same number of nodes. Its advantage over ACD is that the metric value does not need to be put in relation to the number of nodes in the graph. An ACD of 50 is high for a system with 100 elements but quite low for a system with 1,000 elements.
通过将依赖图的CCD值除以节点数相同的平衡二叉树的CCD值来计算NCCD。 与ACD相比,它的优势在于,度量值不必与图形中的节点数有关。 对于具有100个元素的系统,ACD为50会很高,但是对于具有1,000个元素的系统,ACD会非常低。
平衡二叉树的CCD:由n个组件组成的完全平衡的二叉依赖树的CCD为(n + 1)* log2(n + 1)-n
同意避免循环编译时依赖是一个好主意。 找到并打破它们则是另一个故事。
找到它们的唯一真正的选项是使用依赖性分析工具。 对于Java,有一个简单的免费工具,称为“ JDepend” [JDP]。 如果您的项目不是很大,还可以使用“ SonarJ” [SON]的免费“社区版”,它比JDepend强大得多。 对于更大的项目,您需要购买SonarJ的商业许可证。 如果不使用Java或寻找更复杂的功能(如循环可视化和分解建议),则必须使用商业工具。
找到循环依赖关系后,你必须决定如何打破它。 代码重构可以打破组件之间的任何循环编译时依赖性。 最常用的重构方法是添加接口。 以下示例显示了应用程序的“ UI”组件和“ Model”组件之间的不良循环依赖关系:
The example above shows a cyclic dependency between “UI” and “Model”.Now it is not possible to compile, use, test or understand the “Model” component without also having access to the “UI” component. Note that even though there is a cyclic dependency on the component level, there is no cyclic dependency on the type level.
上面的示例显示了“ UI”和“模型”之间的循环依赖关系。现在,如果无法访问“ UI”组件,则无法编译、使用、测试或理解“模型”组件。 请注意,即使在组件级别上具有循环依赖性,在类型级别上也没有循环依赖性。
将接口“ IAlarmHander”添加到“模型”组件即可解决该问题,如下图所示:
现在,“ AlarmHandler”类仅实现了“ Model”组件中定义的接口。 通过用“实现”依赖项替换“使用”依赖项,可以反转依赖的方向。 这就是为什么该技术也被称为“依赖倒置原则”的原因,最早由Robert C. Martin [ASD]描述。 现在,可以隔离地编译、测试和理解“模型”组件。 而且,仅通过实现“ IAlarmHandler”接口就可以重用组件。 请注意,即使该方法在大多数情况下都能很好地工作,过度使用接口和回调也会带来不良的副作用,例如增加复杂性。 因此,下一个示例显示了另一种打破循环的方法。 在[LSD]中,您将找到几种其他的编程技术来打破循环依赖。
在C ++中,可以通过编写仅包含纯虚函数的类来模仿接口。
有时,您可以通过重新改编类的功能来中断周期。 下图显示了一种典型情况:
“订单”类引用“客户”类。 “客户”类还在便捷方法“ listOr ders()”的返回值上引用“订单”类。 由于这两个类位于不同的程序包中,因此会产生不希望的程序包循环依赖性。
通过将便捷方法移至“ Order”类(将其转换为静态方法)来解决该问题。 在这种情况下,对循环中涉及的组件进行调级很有帮助。 在该示例中,很自然地假设订单是比客户更高级别的对象。 订单需要了解客户,但是客户不需要知道订单。 建立级别后,只需要将所有依赖项从低级对象削减到高级别对象。 在示例中,这就是从“客户”到“订单”的依赖关系。
重要的是要提到我们在这里不考虑运行时(动态)依赖性。 出于大规模系统设计的目的,仅编译时(静态)依赖项是相关的。
像Spring框架[SPG]这样的控制反转(IOC)框架的使用将使得避免循环依赖性和减少耦合变得容易得多。
主动管理依赖关系需要定义软件系统的逻辑架构。 逻辑架构将诸如类,接口或包(C#和C ++中的目录或名字空间)之类的物理(编程语言)级元素分组为诸如层,子系统或垂直切片之类的高层架构制品。
逻辑架构定义了这些工件,物理元素(类型,包等)到这些制品的映射以及架构制品之间允许和禁止的依赖关系。
具有层和切片的逻辑架构示例
以下是架构制品的列表,可以使用它们来描述应用程序的逻辑架构:
架构制品 | 描述 |
---|---|
垂直切片 | 尽管许多应用使用水平分层,但是大多数软件架构师都忽略了垂直切片的清晰定义。 功能方面应确定应用的垂直组织。 典型的切片名称为“客户”、“合同”、“框架”等。 |
子系统 | 子系统是最小的架构制品。 它把实现特定的主要技术功能的所有类型组合在一起。 典型的子系统名称为“ Logging”、“ Authentication”等。子系统可以嵌套在层和切片中。 |
自然子系统 | 图层和切片之间的交点称为自然子系统。 |
子项目 | 有时项目可以分为几个相互关联的子项目。 子项目对于组织最高抽象级别的大型项目很有用。 建议在一个项目中存有多个子项目。 |
如果需要,可以嵌套图层和切片。 但是,为简单起见,建议不要使用多于一层的嵌套。
为了简化代码导航和将物理实体(类型,类,包)映射到架构制品的过程,强烈建议对包(C ++或C#中的名字空间或目录)使用严格的命名约定。 一种行之有效的最佳实践是在包名称中嵌入架构制品的名称。
例如,可以使用以下命名约定:
com.company.project.[subproject].slice.layer.[subsystem]…
方括号中的部分是可选的。 对于不属于任何层或切片的子系统,可以使用:
com.company.project.[subproject].subsystem…
当然,如果使用层或切片的嵌套,则需要调整此命名约定。
大多数人不考虑应用程序安全性与应用程序的架构(依赖结构)之间的联系。 但是经验表明,潜在的安全漏洞在遭受架构侵蚀的应用程序中更为常见。 这样做的原因很明显:如果依赖关系结构被破坏并且充满循环,则很难在应用程序内部跟踪受污染数据(来自外部的不受信任数据)的流向。 因此,在由应用程序处理这些数据之前,验证这些数据是否已正确验证也变得更加困难。
On the other hand, if your application has a well-defined logical architecture that is reflected by the code, you can combine architectural and security aspects by designating architectural elements as safe or unsafe. “Safe” means that no tainted data are allowed within this particular artifact. “Unsafe” means that data flowing through the artifact is potentially tainted. To make an element safe, you need to ensure two things:
另一方面,如果你的应用具有代码所反映的定义良好的逻辑架构,则可以结合架构和安全方面通过将体系结构元素指定为安全或不安全。 “安全”是指在此特定制品中不允许污染数据。 “不安全”表示流过制品的数据可能受到污染。 为了使元素安全,您需要确保两件事:
与必须假设整个代码库可能不安全相比,这(使用依赖项管理工具)进行检查和执行要容易得多。依赖项管理工具通过验证所有传入的依赖项仅使用正式入口点,在确保元素安全方面起着重要作用。绕过这些入口点的传入依赖项将被标记为违规。
当然,实际的数据处理只能在“安全”的架构元素中完成。通常,您会将Web层视为“不安全”,而包含业务逻辑的层应全部为“安全”层。
由于许多应用都遭受或多或少的严重架构侵蚀,因此很难使其免受潜在的安全威胁。在这种情况下,你可以尝试使用依赖性管理工具来减少架构侵蚀并创建“安全”处理内核,也可以依靠专门用于查找潜在漏洞的昂贵的商业软件安全性分析工具。虽然第一种方法在短期内会花费您更多的时间和精力,但实际上可以通过提高代码的可维护性和安全性来获得良好的回报。第二种方法更像是一个短期补丁,无法解决根本原因,这是代码库的架构侵蚀。
达到较高技术质量的最佳方法是将一小组规则与基于自动化工具的规则检查和执行方法相结合(有关建议,请参见规则T2。通常,至少应在检查过程中自动检查规则)如果可能的话,规则检查器也应该成为开发人员环境的一部分,以便开发人员甚至可以在将更改提交到VCS [SON]之前检测到违反规则的情况。
因此,建议的规则集是出于意图的简约,可以在需要时进行自定义。经验表明,尽量减少规则数量总是一个好主意,因为这样可以更轻松地在开发过程中检查和执行规则。添加的规则越多,每条附加规则将带来的附加利益越少。这里介绍的规则集基于常识和经验,已经被许多软件开发团队成功实施。
不幸的是,本文没有足够的空间来更详细地解释规则。有关更多信息,请参考最后的参考部分。有些规则似乎是任意的。在这种情况下,可以假定它们源自常识和最佳实践。当然,也可以自由调整阈值和规则以更好地匹配你的特定环境。
规则分为三个优先级类别:
级别 | 建议 |
---|---|
主要规则 | 必须始终遵循 |
次要规则 | 强烈建议您遵循此规则。 如果不可能或不希望这样做,则必须记录原因 |
指导方针 | 建议遵循此规则 |
这些规则涵盖了系统的大规模架构方面。
T4: 根据逻辑架构定义测试。测试设计应考虑整体逻辑架构。 为所有“数据传输对象”创建单元测试,而不是提供业务逻辑的测试类是没有用的。 一个项目应该在最低层次测试上建立明确的规则,而不是盲目地创建测试。推荐的规则是: *为所有与业务相关的对象提供单元测试。独立地测试业务逻辑。 *对已发布的接口进行单元测试。 总体目标是拥有良好的直接和间接测试覆盖率。
Minor Rules
如果你正在开始一个新项目,或在一个现有项目上进行工作,或想要改善组织中的开发过程,则本文是一个很好的起点。 如果你实施并执行上述大多数规则,则可以期望在开发人员生产力,系统可维护性和技术质量方面有显着改善。 尽管这会在一开始就花费很多精力,但总体节省额要比最初的努力大得多。 因此,对于每个专业软件开发组织来说,采用设计和质量规则不仅是“必备”,而且是强制性的。