在本系列的第一期“ 研究体系结构和设计 ”中,我断言每个规模很大的项目都包含没有人想到的设计元素。 当您深入研究问题的细节时,发现您认为很难的事情比预期的要容易得多,反之亦然。 随后的各期文章演示了揭示隐藏但有趣的设计元素的技术。 在本文中,我将这些想法结合在一起,并提供了扩展的案例研究,使用工具和方法来发现代码库中被忽略但重要的部分。
我在“ 组合方法和SLAP ”中介绍了惯用模式的概念。 与《四人帮的设计模式》一书(请参阅参考资料 )所推广的正式设计模式相反,惯用模式并不适用于所有项目。 但是它们无处不在,在您的代码中代表了常见的设计习惯。 它们的范围可以从纯粹的技术模式(例如,项目处理交易的方式)到问题域的模式(例如“在处理订单之前始终检查客户的信用”)。 发现这些模式是紧急设计的关键。
支持大型设计的设计方法在编码开始尝试确定手头应用程序的所有必需设计元素之前要花费大量时间。 他们记录的大部分内容对于解决方案的整体设计都很重要。 但是,实施软件会发现很多惊喜。 您实现的每个设计元素都与其他设计元素耦合,从而创建了极其复杂的依赖关系和关系网。 一旦实现了系统的所有其他必需部分,您认为微不足道的代码方面将变得更加复杂。 无法理解代码中不同设计元素之间的复杂相互作用会导致在估算完成解决方案所需的工作量方面遇到巨大的困难。 准确地来说,估算仍然是软件中的一个黑手艺,因为很难理解和分析这种复杂的耦合和交互蜘蛛网。
依赖紧急设计的敏捷方法尝试了另一种方法。 敏捷的体系结构和设计不会在编码之前就避开设计,但是他们的从业人员发现,只有在实现了整个过程的一个重要部分之后,您才能理解问题的全部范围。 通过开发紧急设计技能,您可以推迟决策,直到拥有更多上下文为止。 精益软件运动(请参阅参考资料 )具有一个伟大的概念,称为最后一个负责时刻 :将决策推迟到最后一个时刻,而要推迟到最后一个负责时刻。 您可以推迟进行设计决策的时间越长,拥有的信息就越多,从而可以进行更细微和具体化的决策。
收获惯用模式
紧急设计意味着可以在现有代码中找到设计元素。 您可以将这些元素视为具有重用潜力的有效抽象。 一种用于收集那些惯用模式的技术使用多种指标。 为了说明这种技术,我将使用Apache Struts代码库(如上一期中所述)(请参阅参考资料 )。 我之所以使用Struts,并不是因为我认为它有缺陷(实际上恰恰相反),而是因为它是众所周知的开源软件。 我认为每个代码库都包含惯用模式,因此任何项目都可以。
使用指标,redux
在“ 通过度量进行紧急设计 ”中,我讨论了如何使用度量来发现不熟悉的代码库的有趣部分,并将其作为重构以改进设计的目标。 我使用了两个指标: 圈复杂度和传入耦合 。 圈复杂度纯粹是一种方法相对于另一种方法的相对复杂性的度量。 因此,仅当与其他环复杂性度量相比时才有意义。 但是,可以合理地指出,具有较低圈复杂度的方法通常不太复杂。 传入耦合表示通过字段或参数引用其他几个类别的计数。 我用的是CJKM度量工具(请参阅相关信息 )对Struts的代码库,收集这些数字。
针对Struts 2代码库运行这两个指标,将在图1中生成表,其中仅显示了有问题的两个指标:
图1. ckjm度量结果在一个表中
图2显示了同一表,按每类加权方法(WMC)排序:
图2. ckjm指标,按WMC排序
仅通过查看此结果,就可以知道DoubleListUIBean
类是Struts代码库中最复杂的类。 这表明重构是消除某些复杂性并查看是否可以找到一些抽象的重复模式的理想选择。 但是,WMC编号并不能告诉您是否将投资此类重构为更好的设计是否是对时间的有效利用。 请注意该类的Ca(afferent耦合)度量标准,其值为3。这意味着只有三个其他类使用该类。 花费大量时间来改进此类的设计可能不值得。
图3显示了相同的CKJM结果,这次按Ca排序:
图3. ckjm结果,按Ca排序(afferent耦合)
这种组合视图表明,Struts中使用最多的类是Component
(鉴于Struts是Web框架,这并不奇怪)。 尽管Component
不像DoubleListUIBean
那样复杂,但它被177个其他类使用,这使其成为改进设计的理想选择。 更好地设计Component
会对许多其他类产生连锁React。
图3中显示的视图使您可以并排查看引用的复杂性和数量。 要查找具有设计挑战的类,请寻找数字的高组合 (这意味着许多其他类使用了复杂的类)。 我研究的主要候选人是UIBean
类,它的循环复杂度为53,传入耦合为22。这是许多其他类使用的复杂类,因此我将对其进行进一步研究。
圈复杂度数ckjm报告表示该类中所有方法的复杂度之和。 我想确定是什么使此类如此复杂,所以我需要为方法使用单独的复杂度数字。 在此单个类上运行JavaNCSS(一种开放源码的圈复杂性工具)(请参阅参考资料 ),结果如图4所示:
图4. UIBean
类的各个复杂度数字
到目前为止,最复杂的方法是evaluateParams()
,其复杂度为43(以及大部分代码行)。 这种方法显然可以处理作为请求的一部分传递给Struts控制器的额外参数的常见情况,将参数类型分配给实际的Struts类和组件。 此代码中存在许多结构重复,如清单1所示:
清单1.valuateParams evaluateParams()
方法的部分内容显示了结构重复
if (label != null) {
addParameter("label", findString(label));
}
if (labelPosition != null) {
addParameter("labelposition", findString(labelPosition));
}
if (requiredposition != null) {
addParameter("requiredposition", findString(requiredposition));
}
if (required != null) {
addParameter("required", findValue(required, Boolean.class));
}
if (disabled != null) {
addParameter("disabled", findValue(disabled, Boolean.class));
}
if (tabindex != null) {
addParameter("tabindex", findString(tabindex));
}
if (onclick != null) {
addParameter("onclick", findString(onclick));
}
// much more code elided for space considerations
这段代码是改进的候选人(请参阅后面的部分, 改进代码,第1部分 ),但是我想进一步探讨一下该代码存在的原因以及可能包含如此多的复杂性。
查看圈复杂度和传入耦合的其他高组合,我发现WebTable
值分别为33和12。 在其上运行JavaNCSS证实了我的怀疑:其第二个最复杂的方法是evaluateExtraParams()
。 我在这里看到图案! 在许多不同的类中看到这种重复的复杂元素,使我怀疑参数周围存在许多偶然的复杂性,因此我进行了一个实验。 通过使用一些UNIX®命令行魔术,我看一下Struts中有多少个类具有名为evaluateParams()
或evaluateExtraParams()
:
find . -name "*.java" | xargs grep -l "void evaluate.*Params" > pbcopy
这个命令在当前目录下的所有Java™源文件向下,并为每个找到的文件,它的任何方法定义,与开始的文件中搜索evaluate
,并结束与Params
。 重定向的最后一位( >
)将结果文件列表粘贴到剪贴板上(至少在Mac上)。 当粘贴结果时,我会感到惊讶:
AbstractRemoteCallUIBean.java
Anchor.java
Autocompleter.java
Checkbox.java
ComboBox.java
DateTimePicker.java
Div.java
DoubleListUIBean.java
DoubleSelect.java
File.java
Form.java
FormButton.java
Head.java
InputTransferSelect.java
Label.java
ListUIBean.java
OptionTransferSelect.java
Password.java
Reset.java
Select.java
Submit.java
TabbedPanel.java
table/WebTable.java
TextArea.java
TextField.java
Token.java
Tree.java
UIBean.java
UpDownSelect.java
所有这些类中都包含一个或两个方法! 我发现了惯用模式。 显然,Struts中的许多类都需要重写和自定义如何处理参数的行为,并且所有这些类本身都处理自定义案例。 现在的问题是:我该如何做得更好?
改进代码,第1部分
在UIBean
evaluateParams()
方法中,您可以看到许多结构重复,我的一位同事将其称为“相同的空白,不同的值”。 换句话说,结构是相同的,只是替换了不同的类或变量名。 这代表了代码的味道,因为您在整个应用程序中本质上都是复制粘贴的代码,只有很小的变化。
修复结构重复的常用技术是使用元编程将重复的结构封装在一个位置。 使用反射来提供不同的所需值,清单2展示了一个新方法和一个validateParams evaluateParams()
方法的改进前奏:
清单2.元编程删除的结构重复
protected void handleDefaultParameters(final String paramName) {
try {
Field f = UIBean.class.getField(paramName);
if (f.get(this) != null)
addParameter(paramName, findString(f.get(this)));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
public void evaluateParams() {
addParameter("templateDir", getTemplateDir());
addParameter("theme", getTheme());
String[] defaultParameters = new String[] {"label", "labelPosition", "requiredPosition",
"tabindex", "onclick", "ondoubleclick", "onmousedown", "onmouseup", "onmouseover",
"onmousemove", "onmouseout", "onfocus", "onblur", "onkeypress", "onkeydown",
"onkeyup", "onselect", "onchange", "accesskey", "cssClass", "cssStyle", "title"};
for (String s : defaultParameters)
handleDefaultParameters(s);
清单2中的handleDefaultParameters()
方法将原来的重复结构封装为单个if
语句。 它接受一个指定Struts参数名称的参数,并使用反射以编程方式获取适当的字段。 然后,它将对原始代码进行null
检查,最后调用Struts addParameter()
方法。
一旦有了handleDefaultParameters
方法,就可以大大减少原始代码行的数量(以及循环复杂度)。 我为每个适用的Struts参数名称创建一个String
数组,并对该数组进行迭代,并在每个数组上调用handleDefaultParameters()
方法。
通过将所有参数检查合并到一个简洁的位置,我所做的不只是减少方法的大小。 原始方法的循环复杂度为43。 if
块采用3行代码(并贡献了1个循环复杂度点), if
每个方法都具有。 我用一个9行的方法(循环复杂度为4)删除了重复项,并消除了66行代码(22个参数x 3行)。 这意味着对于新方法,这种简单的更改从此类中删除了57行代码,并将循环复杂度降低了18点(1 CC点x 22参数-4 CC点)。 对于这么小的更改,我极大地提高了应用程序的可读性,指标,大小和可维护性。 如果将来我需要更改Struts addParameter()
方法的调用方式,则可以在一个地方进行。
这是一个短期修复,但我将其说明来说明简单的更改如何对代码的清洁度产生深远的影响。 但是,如果这是我的代码库,我将制定长期解决方案。
改进代码,第2部分
如果这是我的项目,我会将整个参数处理机制抽象为其自己的类集,实质上是在Struts中构建子框架。 处理参数的代码的复杂性及其普遍性和数量性表明,应将其视为Struts中的一等公民。 这样做不在单篇文章的讨论范围之内,但是您可以看到Struts的大量复杂性(基于指标)都围绕着这个问题展开。
紧急设计和惯用模式
您是否认为Struts的原始设计师曾经梦想过要处理参数多少代码? 软件就是这样。 有时您可以基于对问题域的推测性知识来预测复杂性,但是编写代码会创建新的约束和机会,这些约束和机会实际上是无法预测的。 实际上,高级开发人员在预测困难方面并没有得到更好的帮助。 他们会更好地猜测神秘的硬物最终会抬起头来。
出现紧急设计的部分原因是认识到我们无法可靠地预测将要遇到的困难,但是我们应该对此保持警惕。 如果您期望在代码库中找到抽象和模式,那么它们就会更容易看到。
我基于一个间歇性工作的ThoughtWorks项目完成了一个案例研究。 在这个大型Ruby on Rails项目的早期,技术负责人意识到我们需要在少数几个孤立的情况下进行异步行为(例如,当上传大量图像时,用户希望能够离开页面并稍后返回状态)。 如果我们有一个大的设计前心态,我们将立即进入消息队列。 但是在项目开始时,当我们不知道所有需要异步的事物时,默认位置是获取我们可以找到的最复杂的消息队列,以确保它可以处理将来的新需求。 但是技术负责人没有做到这一点。 他认为我们所拥有的足以应付当前的情况。
快进两年了。 至此,该应用程序具有三种不同的异步行为,当前的解决方案开始成为瓶颈。 现在是时候获取消息队列了。 但是由于技术负责人推迟了这么长时间的决策,因此我们确切地知道了此应用程序在消息传递方面的需求,从而使我们能够获得完成这项工作的最简单工具。 通过等待到最后一个负责的时刻,我们节省了避免因复杂性而导致的复杂性,而这种复杂性是由比我们需要的工具更复杂的工具所产生的,从而使代码更简洁,新功能的运行速度更快,并且不需要烦人的工作。
允许代码引导您进行设计,意味着您可以更好地了解自己的需求。 您可以推迟设计决策的时间越长,一旦决定要做出具有长期影响的决策,路径就越清晰。
结论
本系列文章的大部分内容都是将上下文加载到您的头脑中,以便可以显示出真正的好处。 本期文章将利用该系列文章中的技巧,工具和态度,将本系列中的每篇以前的文章几乎都联系在一起。 紧急设计需要具有查看和收集惯用模式和抽象的能力,以及在这些事物出现时能够利用它们的工具和技术。 预先设计可以帮助您发现重要的部分(敏捷项目预先进行足够的设计来确定那些事情),但是一旦开始编码,请保持开放的思想,这将使您获得令人惊讶的重要设计元素。 每个代码库都有惯用模式。 您只需要学习了解他们并采取行动。
在过去的几期中,我主要介绍了设计和相关问题。 下次,我将深入探讨演化体系结构,并展示一些敏捷开发技术与体系结构概念结合在一起时出现的常见问题和解决方案。
翻译自: https://www.ibm.com/developerworks/java/library/j-eaed9/index.html