65.9K
CodeProject 正在变化。 阅读更多。
Home

策略模式入门(通俗易懂)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (63投票s)

2010年3月19日

CPOL

7分钟阅读

viewsIcon

59528

downloadIcon

277

本文采用基于场景的方法,让读者理解这种模式。

引言

关于这个主题已经有很多阐述,而且有些写得相当精彩,那么为什么还要写另一篇文章呢?我认为大多数的阐述对于初学者来说都过于抽象,他们往往纠结于定义和问题陈述,而未能真正领会其强大的功能和应用的可能性。这是我谦逊的尝试,目标读者不是经验丰富的程序员,而是那些不太熟练的程序员;当然,一些经验丰富的程序员也可能会从中受益。我会尽量让内容保持简单。我选择策略模式作为我写的第一种模式,因为它是我最常遇到的,而且我认为它是最广泛使用的设计模式之一。策略模式的类图如下。

image001.png

定义

首先,一个正式的定义

策略模式是一种将算法与其宿主类解耦的技术,这样我们就可以在运行时选择一种特定的算法。我们的宿主(即上下文)类不再关心具体的实现是什么,因为它将处理抽象。

如果在这个阶段你连一个词都听不懂,也没关系。我希望在阅读下一节之后,如果你再读这个定义,它不仅会让你茅塞顿开,而且你还会明白在哪里应用这种模式。不过,我假设读者对多态、继承、接口和抽象类等面向对象术语非常熟悉。

思考过程

考虑这样一个场景:你正在开发一个可重用的电子病历 API,你当前的任务是实现一个计算体表面积 (BSA) 的模块,该模块基于 `VitalSigns` 类中的两个因素:身高和体重。

VitalsSimple.jpg

请参考 http://www.halls.md/body-surface-area/refs.htm 查找不同的可用公式。现在是假设部分。假设目前只知道 Boyd 和 Heycock 两种公式。但是还有一些正在标准化或开发中的公式。对 `VitalSigns` 的一个可能的修改如下:

vitalOld.jpg

乍一看,这个解决方案似乎不错,但它有一个缺陷:它不够灵活。当新的公式标准化后,API 用户将需要在他们的应用程序中获得对这些公式的支持。有了这个实现,你唯一的选择就是给 `CalculateBSA()` 函数添加另一个 case,然后重新编译你的 API 并为他们提供补丁。更糟糕的是,你的 DLL 可能被强命名,导致开发者需要重新编译所有内容。或者 API 用户将不得不从头开始实现大部分功能,而无法从你的 API 中获得任何好处。

如果能以某种方式将这种变化的功能与 `VitalSigns` 类分开管理,并在用户需要 BSA 计算时将其作为依赖项提供,那该多好啊。最好能提供一种机制,让用户可以通过这种机制插入他们自己的实现,这样会非常灵活。这就是策略模式发挥作用的地方。它将使你的解决方案非常灵活,而且你的 `VitalSigns` 类对于新的实现无需任何更改。

现在我们将看看策略模式如何解决这个问题。有问题的函数是 `CalculateBSA`。因此,我们将单独实现它。首先,我们将实现接口(因为它是可扩展性的关键,到最后你就会明白为什么)。
所以,这是你开发的接口:

IBsaCalculator.jpg

注意:通常我们会传递整个上下文对象(在我们的例子中是 `VitalSigns` 实例)。关于选择像这样标量值还是整个上下文的争论,与跨不同层的标量值或实体类的选择 pretty 相似。我会在最后讨论这一点,所以现在你可以 pretty 好地忽略这个注释 ;) 。

你的每个公式都将成为该接口的具体实现,例如,Boyd 公式:

Boyd_BSA_Calculator.jpg

由于所有具体实现都派生自 `IBsaCalculator`,因此所有这些都可以被视为 `IBsaCalculator` 本身。为了适应我们的变化,我们将按如下方式更新我们的 vital signs 类:

VitalSigns_New.jpg

在 `CalculateBSA()` 内部,我们调用了接口的 `CalculateBSA(double, double)`,这显然会以多态的方式被调用。

我们取得了什么成就

这个解决方案 pretty 灵活。让我举个例子来阐明这一点。假设你的 API 发布后的第二天,Dubois 公式就发布了。现在用户不必等待新版本,他们可以通过派生自 `IBsaCalculator` 并将其分配给 `VitalSigns` 实例的 `BsaCalculator` 属性来在他们的应用程序中实现它。他们所要做的就是提供 `IBsaCalculator` 的另一个具体实现。

Dubios.jpg

我们无需修改 `VitalSigns` 类即可实现新功能,并且可以根据需求在运行时插入不同的计算器实现。

谁将提供依赖项?

没有绝对的解耦。依赖项就像物质一样,是不可摧毁的。经验丰富的程序员只是通过重塑它们来获得更好的可扩展性和可维护性解决方案。例如,当你使用 Spring 这样的框架时,依赖项并不会凭空消失。你在 XML 文件中声明它们,而不是将它们硬编码到你的代码中,现在,依赖项是由框架提供的,而不是你的代码,这使得你的类彼此解耦。问题是,我们应该在哪里向 `VitalSigns` 类提供具体的实现?显然,现在,客户端(我说的客户端是指将要使用这一切的代码)必须了解这些具体的实现,并且它负责提供所需的计算器。我们可以将创建任务委托给工厂方法来增加另一层抽象,但让我们不要过度复杂化,而是坚持一个简单的实现。让我解释一下这一点。假设我们在 `ComboBox` 中显示所有可用的公式。

FormulaChoice.jpg

用户选择的事件处理代码将如下所示:

InstantiatingFormula.jpg

这里 `vs` 是我们的上下文类(`VitalSigns`)的实例。
所以,我们已经看到了策略模式如何使我们的 `VitalSigns` 类具有可扩展性。我们不仅可以插入自己的实现,还可以根据需要提供和更改运行时选择。

另一种可能的情况

我不会花太多时间在这个场景上,留给读者作为练习。假设你有一个庞大的类层次结构,例如,就像世界著名的 `Vehicle` 类一样,它有 car、truck、wagon 等派生类。每个派生类又有许多进一步的派生类,依此类推。假设你需要在该层次结构中的大多数类中覆盖 `Move()` 函数。这将使你的 `Move()` 实现散布在整个层次结构中,使其非常难以维护。因此,你可以使用策略模式,将 Move 实现分离出来,并单独实现它。在这里,我们主要获得的不是**可扩展性**,而是**可维护性**。

标量值还是上下文类本身?

在我的实现中,我使用了标量值 `height` 和 `width`

IBsaCalculator.jpg

通常人们更喜欢传递整个上下文,例如 `CalculateBSA(VitalSigns vs)`。

两者都有其优缺点。标量值将使此实现与 `VitalSigns` 完全解耦,这在某些情况下是有益的,但如果将来开发了一个消耗其他 `VitalSigns` 属性(例如,另外还有温度)的公式,它将破坏该实现。使用上下文类会使其与 `VitalSigns` 耦合,但会避免这类更改。这是一个选择问题,取决于你的需求。

关于本文

本文以及本系列后续的文章绝不是设计模式的全面指南。它们主要面向初学者,向他们解释设计模式并非什么高深莫测的科学,而只是简单优雅的解决方案,可以避免未来的重构和维护。目标是尽可能简单地介绍这些模式,避免任何复杂的实现。只关注手头的模式,以免新手感到困惑。到本系列结束时,我将尝试撰写关于如何将这些模式联系起来以创建灵活且可维护的应用程序框架。例如,我们可以创建一个 Factory 来实例化具体实现并使客户端与创建过程解耦,但与其在这里介绍,我将在 Factory 模式的教程中进行解释,以免读者感到困惑。您的反馈将非常重要,告诉我本文的不足之处以及我未能澄清概念的地方。谢谢。

关于提供的代码

我附带的这篇文章的代码是在 C# 中使用 Visual Studio 2008 开发的。但是,如果需要,我可以提供使用 Eclipse 或 NetBeans 的 Java 实现,甚至 C++ 版本。

历史

  • 2010年3月19日:初始发布
© . All rights reserved.