在C++应用程序中应用策略模式






4.71/5 (15投票s)
2001年1月9日

129340

1165
当一个过程可以有多种不同的算法来实现时,可以使用策略模式来确定最佳解决方案。
引言
软件咨询公司会根据“固定价格”或“按时计费”的基础为客户提供项目。此外,项目可以是现场(onsite)或远程(offsite)。通常,客户会指定他们希望项目如何进行(固定价格或按时计费,现场或远程)。咨询公司的最终目标是在预定时间内完成项目,但根据项目执行方式的不同,他们采用的策略(或策略)可能会有所不同。这是一个实际生活中的例子,其中应用了策略模式。
策略模式也可以用于软件设计。当一个过程可以有多种不同的算法来实现,并且每种算法都取决于具体情况的最佳解决方案时,就可以使用策略模式。本文将详细介绍策略模式。它使用编程示例来解释策略模式是什么、何时以及为何需要它。文章讨论了使用策略模式进行软件设计的优点和缺点。还介绍了在 C++ 中实现该模式的三种不同方法以及策略模式的已知用法。
设计模式旨在为沟通设计原则提供通用词汇。在《《可复用的面向对象软件设计模式:Erich Gamma 等人著(Addison-Wesley,1995)》》一书中,策略模式被归类为行为模式。在本文中,我将使用“设计模式四人组(GoF)”使用的术语来解释策略模式。
一个示例
进度指示器是一个应用程序可以用来指示长时间运行操作(例如,安装过程)进度的窗口。它通常是一个矩形窗口,在操作进行时,该窗口会从左到右逐渐被高亮色填充。它有一个范围和一个当前位置。范围代表操作的整个持续时间,当前位置代表应用程序在完成操作方面取得的进展。范围和当前位置用于确定进度指示器中要填充高亮色的百分比。
尽管从左到右的填充方向在大多数进度指示器中都很常见,但也可以使用其他方向,例如从右到左、从上到下和从下到上进行填充。我见过一些进度指示器使用从下到上的填充方式。此外,对于给定的填充方向,可以使用不同类型的填充,例如连续填充、断续填充或基于图案的填充。
简而言之,进度指示器的目的保持不变;然而,填充方向或填充算法可以改变。因此,用于填充的算法族可以封装在一个单独的填充类层次结构中,应用程序可以配置进度指示器使用一个具体的填充类。以这种方式封装的算法称为策略。那么,什么是策略模式?策略模式是一种设计模式,用于封装变体(算法),并策略性地交换它们,以在不改变其架构的情况下更改系统行为。根据 GoF 的说法,策略模式旨在“定义一系列算法,封装每个算法,并使它们可以相互替换。策略允许算法独立于使用它的客户端而变化”。
策略模式有三个参与者,包括策略(Strategy)、具体策略(Concrete Strategy)和上下文(Context)。在本例中,抽象填充类 `CFiller` 被称为策略,具体填充类 `CLToRFiller`(用于提供从左到右填充)和 `CRToLFiller`(用于提供从右到左填充)被称为具体策略,而进度指示器 `CProgressIndicator` 被称为使用策略的上下文。使用进度指示器的应用程序是上下文的客户端。根据具体情况,客户端会用一个具体的填充类对象(具体策略)来配置进度指示器(`Context`)。
`CProgressIndicator` 维护着一个指向 `CFiller` 对象的引用。每当操作有进展时,应用程序会通知 `CProgressIndicator`(通过调用 `SetPos` 等方法);`CProgressIndicator` 将请求转发给 `CFiller` 对象以在视觉上指示更改。`CFiller` 的子类 `CLToRFiller` 和 `CRToLFiller` 实现填充算法(在 `DoFill` 方法中)。通过将填充算法与进度指示器隔离开来,可以使用新的填充策略,而无需更改进度指示器。将填充算法单独封装可以消除对多个条件语句的需求,以选择正确的填充策略。下面展示了策略模式参与者之间关系的 UML 图。
在 C++ 中实现策略模式的方法
`Push` 和 `Pull` 方法可用于提供安全数据交换的手段,并减少上下文和策略对象之间的耦合。在 `Push` 方法中,`Context` 将数据推送到 `Strategy` 对象,`Strategy` 使用数据来实现算法。在此方法中,`Context` 可能会向 `Strategy` 对象传递一些不需要的数据,因为并非所有 `Concrete Strategy` 对象都需要所有数据。另一方面,在 `Pull` 方法中,`Context` 会将自身注册到 `Strategy`,而 `Strategy` 会维护对 `Context` 对象的引用,并从中拉取所需数据。在此方法中,`Context` 必须定义一套详尽的 `Get` 方法供 `Strategy` 对象拉取所需数据。由于 `Strategy` 维护着对 `Context` 的引用,因此这两个类被紧密耦合。 `Push` 或 `Pull` 方法的选择完全取决于需求。
本文讨论了在 C++ 中实现策略模式的三种不同方法。下面介绍的方法可以使用 `Push` 或 `Pull` 方法。
策略对象作为上下文的必需参数
在此方法中,进度指示器(`Context`)在其构造函数中将一个填充器(`Strategy`)对象作为参数,并维护对其的引用。当调用 `SetPos` 方法时,进度指示器会将请求委托给填充器对象。列表 1 展示了此方法。此外,Java 中的布局管理器 也使用了此方法,请参阅Java 和策略模式以获取解释。
优点
- 进度指示器仅依赖于填充类接口,而不直接与填充类的具体子类进行交互。
- 应用程序可以在运行时选择所需的填充类对象。`SetFiller` 方法可用于在创建进度指示器后更改填充类对象。
缺点
- 使用进度指示器的应用程序必须了解所有填充类,并且必须向进度指示器提供所需的填充类对象。
- 进度指示器不控制填充类对象的范围或生命周期。
策略对象作为上下文的可选参数
此方法与第一种方法类似,但填充对象(`Strategy`)在创建进度指示器(`Context`)时作为可选参数传入。如果应用程序在构造时未指定填充对象,则进度指示器将创建一个默认填充对象(从左到右填充)。列表 2 包含展示此方法的 C++ 示例。本文提供的演示应用程序使用了此技术。
优点
- 应用程序仅在需要更改默认填充对象时才指定填充对象。
- 应用程序可以在运行时选择所需的填充类对象。`SetFiller` 方法可用于在创建进度指示器后更改填充类对象。
缺点
- 进度指示器必须了解具体的填充类 `CLToRFiller`,以便提供默认行为。这增加了 `CProgressIndiator` 和 `CLToRFiller` 类之间的耦合。
- 进度指示器仅控制默认填充对象的生命周期,在本例中是 `CLToRFiller` 对象。但是,它不控制其他填充类对象的范围或生命周期。
策略作为模板类参数
如果不需要在运行时更改填充类(`Strategy`),则可以在编译时将其作为模板参数传递给进度指示器(`Context`)类。列表 3 展示了此方法。Active Template Library (ATL) 使用此方法的变体来选择所需的 `CCOMObjectxxx<>`,其中 `Context` 作为参数传递给 `Strategy` 类(`Pull` 方法)。有关解释,请参阅ATL 和策略模式。
优点
- 进度指示器模板类仅使用具体填充类进行实例化,因此不需要 `abstract CFiller` 类。
- 将填充类作为模板参数传递可在进度指示器和填充类之间实现早期绑定。这避免了运行时开销并提高了效率。
- 进度指示器负责创建填充类对象。因此,它对对象的生命周期拥有完全控制权。
缺点
在编译时选择填充类,运行时无法更改对象。
使用策略模式的好处
- 可以定义一个算法类层次结构,并可互换地使用它们来更改应用程序行为,而无需更改其架构。
- 通过单独封装算法,可以轻松引入符合相同接口的新算法。
- 应用程序可以在运行时切换策略。
- `Strategy` 使客户端能够选择所需的算法,而无需使用“`switch`”语句或一系列“`if-else`”语句。
- 用于实现算法的数据结构完全封装在 `Strategy` 类中。因此,可以在不影响 `Context` 类的情况下更改算法的实现。
- 可以使用策略模式代替继承 `Context` 类。继承会将行为硬编码到 `Context` 中,并且行为无法动态更改。
- 同一个 `Strategy` 对象可以在不同的 `Context` 对象之间策略性地共享。但是,共享的 `Strategy` 对象在调用之间不应维护状态。
使用策略模式的缺点
- 应用程序必须了解所有策略,才能为特定情况选择正确的策略。
- `Strategy` 和 `Context` 类可能被紧密耦合。`Context` 必须向 `Strategy` 提供相关数据以实现算法,有时 `Context` 传递的所有数据可能并非所有 Concrete Strategies 都需要。
- `Context` 和 `Strategy` 类通常通过 `abstract Strategy` 基类指定的接口进行通信。`Strategy` 基类必须公开所有必需行为的接口,而某些具体 `Strategy` 类可能不实现这些接口。
- 在大多数情况下,应用程序会用所需的 `Strategy` 对象配置 `Context`。因此,应用程序需要创建和维护两个对象而不是一个。
- 由于 `Strategy` 对象通常由应用程序创建,因此 `Context` 无法控制 `Strategy` 对象的生命周期。但是,`Context` 可以创建 `Strategy` 对象的本地副本。但这会增加内存需求并对性能产生影响。
已知用法
本节介绍策略模式的已知用法。本节介绍的一些已知用法摘自 GoF 的设计模式书籍。
ATL 和策略模式
ATL 代表 Active Template Library。它是一组基于模板的类,旨在隐藏 COM 开发的大部分复杂性,并为组件本身提供小巧的占用空间。
在 ATL 中,COM 对象的类不是直接实例化的。它充当 `CComObjectxxx<>` 类的基类。例如,如果 `CMyClass` 是 COM 对象类,则最派生的类将是 `CComObjectxxx
ATL 使用策略模式来封装不同 `CComObjectxxx<>` 类中的行为,COM 类可以根据所需的功能选择所需的 `CComObjectxxx<>`。由于不需要在运行时更改 `Strategy`,因此 ATL 使用 C++ 模板来实现策略模式。ATL 选择一个 `Strategy`(`CComObjectxxx<>`)并将 `Context`(`CMyClass`)作为参数传递给 `Strategy`。
Java 和策略模式
策略模式也用于 Java 中布局管理器 (Layout Manager) 的实现。布局管理器可以配置一个布局对象,该对象可以是 `FlowLayout`、`CardLayout`、`GridLayout` 或 `GridBagLayout` 类的一个对象。这些类封装了用于布局可视化组件的算法,并为查看相同的可视化组件提供了多种不同的布局。
其他已知用法
Borland 的 `ObjectWindows` 使用策略来封装对话框输入字段的验证算法。例如,数字字段可能有一个验证器来检查范围是否正确,日期字段可能有一个验证器来检查输入日期的正确性,而字符串字段可能有一个验证器来检查语法是否正确。
ET++ 使用策略模式来封装文本查看器的布局算法。
策略模式也用于许多流行的排序算法、图布局算法和内存分配算法。
桥模式与策略模式
通常,策略模式与桥模式 (Bridge Pattern) 混淆。尽管这两种模式在结构上相似,但它们试图解决两个不同的设计问题。`Strategy` 主要关注封装算法,而 `Bridge` 将抽象与实现解耦,以提供相同抽象的不同实现。
摘要
本文重点介绍了策略模式,不仅阐述了策略模式是什么,还强调了为什么以及何时需要它。我在许多项目中都使用过这种模式,包括实现词法分析器和解析器类。只要有多种执行相同任务的方法,就可以应用这种模式。简而言之,策略模式可用于封装变化的算法,并使用它们来更改系统行为,而无需更改其架构。
致谢
特别感谢我的朋友 Sree Meenakshi,感谢她为改进本文的清晰度和呈现方式提出的宝贵建议。
列表 1 - 策略对象作为上下文的必需参数
// Forward declaration for CFiller class
class CFiller;
// Class declaration for CProgressIndicator
class CProgressIndicator
{
// Method declarations
public:
CProgressIndicator(CFiller *);
INT SetPos(INT);
INT SetFiller(CFiller *);
…
…
// Data members
protected:
CFiller * m_pFiller;
};
// CProgressIndicator - Implementation
CProgressIndicator ::CProgressIndicator(CFiller * pFiller)
{
// Validate pFiller
ASSERT(pFiller != NULL);
m_pFiller = pFiller;
}
INT CProgressIndicator ::SetPos(INT nPos)
{
// Some initialization code before forwarding the request to filler object
…
…
// Request forwarding to filler object
INT nStatus = m_pFiller->DoFill(…);
…
…
return nStatus;
}
INT * CProgressIndicator ::SetFiller(CFiller * pFiller)
{
// Validate pFiller
ASSERT(pFiller != NULL);
// Set new filler object
m_pFiller = pFiller;
return 0;
}
列表 2 - 策略对象作为上下文的可选参数
// Forward declaration for CFiller class
class CFiller;
// Class declaration for CProgressIndicator
class CProgressIndicator
{
// Method declarations
public:
CProgressIndicator(CFiller * = NULL);
virtual ~CProgressIndicator();
INT SetPos(INT);
INT SetFiller(CFiller *);
…
…
// Data members
protected:
CFiller * m_pFiller;
BOOL m_bCreated;
};
// CProgressIndicator - Implementation
CProgressIndicator ::CProgressIndicator(CFiller * pFiller)
{
// Check and create filler object
if(pFiller == NULL)
{
// Create a default Left to Right filler object
m_pFiller = new CLToRFiller;
m_bCreated = TRUE;
}
else
{
m_pFiller = pFiller;
m_bCreated = FALSE;
}
}
CProgressIndicator::~CProgressIndicator()
{
// Delete filler object, only if it is created by the progress indicator
if(m_bCreated == TRUE)
{
delete m_pFiller;
}
}
INT CProgressIndicator ::SetPos(INT nPos)
{
// Some initialization code before forwarding the request to CFiller object
ASSERT(m_pFiller != NULL);
…
…
// Request forwarding to CFiller object
INT nStatus = m_pFiller->DoFill(…);
…
…
return nStatus;
}
INT * CProgressIndicator ::SetFiller(CFiller * pFiller)
{
// Validate Filler object
ASSERT(pFiller != NULL);
// Delete filler object, only if it is created by the progress indicator
if(m_bCreated == TRUE)
{
delete m_pFiller;
m_bCreated = FALSE;
}
// Set new filler object
m_pFiller = pFiller;
return 0;
}
列表 3 - 策略作为模板类参数
template <class TFiller> class CProgressIndicator
{
// Method declarations
public:
INT SetPos(INT);
…
…
// Data members
protected:
TFiller m_theFiller;
};
// CProgressIndicator - Implementation
INT CProgressIndicator ::SetPos(INT nPos)
{
// Some initialization code before forwarding the request to CFiller
// object
…
…
// Request forwarding to CFiller object
INT nStatus = m_theFiller.DoFill(…);
…
…
return nStatus;
}
// Application code using CProgressIndicator
CProgressIndicator<CLToRFiller> LtoRFillerObj;
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。