在工作中,我正在处理庞大的Java代码库,该代码库是由许多不同的开发人员在15年的时间里开发的。 并不是所有的事情都由书来完成,但是同时我通常没有机会重构遇到的每一个奇怪之处。
尽管如此,仍可以每天采取提高代码质量的措施。 今天就像那样……
总览
由于已经 存在 大量 教程 ,因此本文的目的不是教授装饰器模式。 相反,它提供了一个现实生活中的示例,说明它如何派上用场并节省了一天的时间。
情况
我们的UI包含Swing的JEditorPanes ,用于显示HTML。 与各种链接的交互(例如,悬停和单击)触发以下一个或多个响应:
- 记录事件
- 更改光标(JEditorPane已经自行完成;似乎从2000年5月开始-…?!)
- 使用链接的内容更新窗格
- 打开外部浏览器
- 打开外部应用程序
- 处理内部服务请求
对于所有窗格,这些响应都不相同。 其中有一些需求部分不同。 (如果您知道装饰器模式,那么您会看到它的去向。)
所以问题是:您如何实施这些响应?
一种可配置类的解决方案
您可以将所有这些都放在一个类中,该类实现HyperlinkListener
并使用标志来(取消激活)不同的响应。
这个课真是地狱! 是的,地狱 就这么简单。
首先,它将是巨大的。 而且,其本质上不相关的职责之间可能以某种方式有些奇怪的依赖。这种规模和这些关系将使编写和测试变得更加困难,甚至使理解和修改变得更加困难。
(顺便说一句,造成混乱的根本原因是AllInOneHyperlinkListener
违反了单一职责原则 。由于这篇文章已经足够长了,因此我将不做详细介绍。)
继承的解决方案
无论如何,我很幸运没有发现自己正在面对一个庞然大物的听众。 取而代之的是,我发现了一个小的类层次结构,这些类将这些职责划分为它们( HL是HyperlinkListener的缩写):
-
CursorSettingHL implements HL
:记录事件并设置光标 -
UrlProcessingHL extends CursorSettingHL
:
通过更新窗格的内容或打开外部浏览器/应用程序来处理URL -
ServiceRequestHandlingHL extends UrlProcessingHL
:如果是服务请求,则处理URL; 否则委托给它的超类
这看起来更好,不是吗? 好…
首先,某些班级仍然要承担几项责任。 没有真正的理由解释为什么日志和更改游标应该由同一类完成。 (我只能猜测,这种结构会随着时间的推移而有机地增长,而没有任何更深层次的设计。)因此,问题较小,但尚未消失。
它也显示在班级名称中。 以上内容已经过改进,以提高可读性。 原始文档中充满了Default , Simple和其他非信息。 这个名字甚至是误导性的名字都不是简单的疏忽。 它们是缺乏凝聚力的自然结果。
但是通过更深层次的管理,这些问题本来可以得到缓解。 六个类可以各自实现一件事。 但这也不会帮助我。
不,此解决方案的真正问题是模拟的灵活性。 看起来您可以选择,但实际上您不能。 看看当事情改变时会发生什么。
改变
我们慢慢地从摇摆移动到JavaFX的,我想以取代FX JEditorPane中” 的WebView 。 (实际上,将HyperlinkListeners放入WebView有点麻烦,但我将在另一篇文章中再讨论。)WebView已经完成了上述某些操作,因此这是新侦听器具有的更新响应列表。触发:
- 记录事件
改变光标用新内容更新窗格- 打开外部浏览器
- 打开外部应用程序
- 处理内部服务请求
在这里,整个类系统变得毫无用处。 (至少因为我不愿意让监听者对隐身控件进行2.和3.。)在这一点上,很明显,职责混在一起了。 我仍然需要其中一些,但不是全部,而且由于它们之间没有阶级界限,所以我处于全有或全无的情况。
装饰图案的救援
因此,当我在考虑要混合和匹配现有功能的程度时,它最终使我受了折磨(并且比预期的要晚得多):这正是装饰器模式的目的!
装饰图案
就像我说的,我不会详细解释这种模式,但是基本思想是:
当有一个接口,不同的实现可以提供不同的功能时,请让每个实现独立运行。 但是要实现它们,以便在工作中的某个时刻,他们将控制权移交给同一接口的另一个实例。
如果一个这样的实现调用另一个,并使用该结果来计算自己的实现,则两者都可以做自己的事情,但是效果会重叠。 第二个实例的结果仍然存在,但是第一个实例有些改变。 因此,据说第一个装饰第二个。
这可以在更多实例中进行,每个实例都装饰前者。 应该将其视为分层系统,其中每个装饰器向整体添加另一层行为。
行动中
现在方法很清楚:我将上述功能重构为不同的装饰器,例如LoggingHyperlinkListenerDecorator
和ServiceRequestHandlingHyperlinkListenerDecorator
。
然后,我删除了原始类,并用正确的装饰器组合替换了它们的用途。 最终,我了解了新功能并选择了正确的装饰器。 用Java 8可以做到这一点,但是为了简单起见,让我们在这里使用构造函数:
将装饰器放在一起
// use a lambda expression to create the initial listener
// which does nothing
HyperlinkListener listener = event -> {};
// these decorators first do their own thing and then call the
// decorated listener (the one handed over during construction);
// in the end, the last added decorator will act first
listener =
new ExternalApplicationOpeningHyperlinkListenerDecorator(listener);
listener =
new BrowserOpeningHyperlinkListenerDecorator(listener);
listener =
new ServiceRequestHandlingHyperlinkListenerDecorator(listener);
listener =
new LoggingHyperlinkListenerDecorator(listener);
除了样板外,很明显这里发生了什么。 首先,在我们确定服务请求并处理它们之前,将进行日志记录。 如果可能,将在浏览器中打开其他任何内容; 否则,我们会将其交给一些外部应用程序。
效果
您马上就可以看到对代码的积极影响。 首先,每个班级都有一个非常简单的责任。 这导致了简短易懂的课程。 他们的名字通常是当场就正确地告诉您他们在做什么。 另外,由于每个单元中发生的事情更少,因此可测试性也提高了。
此外,将装饰器放在一起的地方更能揭示其意图。 您不必检查实例化的ServiceRequestHandlingHyperlinkListener
及其超类即可了解侦听器的确切功能。 取而代之的是,您仅查看装饰列表,看看会发生什么。
最后但并非最不重要的一点是,它使代码为将来的更改做好了准备。 现在很明显如何实现新的侦听器功能。 对于继承的类,您必须想知道在何处放置新功能,以及新功能如何影响该类的现有用法。 现在,您只需要实现第um装饰器并在需要的地方添加它即可。
反射
这个真实的例子展示了装饰器模式的应用如何使代码更易于阅读,测试和更改。
当然,这不是自动的。 该模式只能在确实使代码更整洁的地方使用。 但是要决定这一点,您必须了解它并且必须能够推理其影响。 我希望这篇文章对此有所帮助。
非常感谢Wikipedia的Benjah ,他创造了 Vaska复杂建筑 的美丽形象并将其发布到公共领域。
翻译自: https://www.javacodegeeks.com/2015/01/how-the-decorator-pattern-saved-my-day.html