策略模式 - Strategy
策略模式是一种行为型设计模式,与创建型设计模式和结构型设计模式不同,策略模式不关注对象的创建、组合、继承或聚合,其主要提供了解决某类问题的特定方法
俗语:劈柴不照纹,累死劈柴人
所谓策略,更通俗的讲是一系列函数,而我们要求这些函数可以适应多种不同的场景,如何便捷高效的实现这一常见诉求,这就是策略模式的目的
函数式策略
函数式策略是一种临时策略,通常以 仿函数 或 lambda表达式 进行策略的传递,这种方式适用于不打算再次使用的情况下使用,事实上这是一种非常常用、方便的策略,对于操作(如排序),只需要提供不同的 函数对象 或者简单的 lambda表达式 就可以实现不同的功能
// 仿函数
vector<int> v1{1, 5, 4, 2, 3};
sort(v1.begin(), v1.end(), less<>{});
for (auto x : v1) {
cout << x << ' '; // 1 2 3 4 5
}
cout << endl;
// lambda 表达式
vector<int> v2{1, 5, 4, 2, 3};
sort(v2.begin(), v2.end(), [=](int a, int b) { return a < b; });
for (auto x : v2) {
cout << x << ' '; // 1 2 3 4 5
}
动态策略
所谓 动态策略,是一种运行时可替换的策略模式,通过维护所需策略的指针或引用来进行策略切换
事实上,最常见的实现方式之一是通过继承关系,重写虚函数来实现,这样以来,多个**子类(具体策略)**通过继承 基类(策略模板) 就可以实现针对不同数据类型的相同操作
// 基类,策略框架,像输出流中输出列表类型的字符串格式
struct ListStrategy
{
virtual ~ListStrategy() = default;
// 像输出流中添加指定元素
virtual void add_list_item(ostringstream& oss, const string& item) {};
// 像输出流中添加起始元素
virtual void start(ostringstream& oss) {};
// 像输出流中添加结尾元素
virtual void end(ostringstream& oss) {};
};
如代码段所示,基类 ListStrategy
并未包含 纯虚函数,因此其并不是一个 抽象基类,这样做的好处是,子类的实现者只需要实现自己所需要的功能函数,而不必理睬那些未涉及部分
// 子类,用于Markdown格式输出策略,markdown不需要开头结尾符号
struct MarkdownListStrategy : ListStrategy
{
void add_list_item(ostringstream& oss, const string& item) override
{
oss << " * " << item << endl;
}
};
// 子类,用于HTML格式输出策略,HTML无序列表标签<ul></ul>,列表项标签<li></li>
struct HtmlListStrategy : ListStrategy
{
void start(ostringstream& oss) override
{
oss << "<ul>" << endl;
}
void end(ostringstream& oss) override
{
oss << "</ul>" << endl;
}
void add_list_item(ostringstream& oss, const string& item) override
{
oss << "<li>" << item << "</li>" << endl;
}
};
如上,MarkdownListStrategy
只需要实现所需方法,假设 ListStrategy
是一个 抽象基类
, 那么我们只能被迫重写 start()
、end()
,即使并不打算实现任何功能
现在我们已经有了应对不同文本类型的策略,但是我们还需要一个可以处理文件流的组件 TextProcessor
,当然我们还需要一个 枚举类,用于指定具体的实现类型,当然也可以通过类似 #define isMarkdown 0
的预处理语句实现相似的功能,但通常来说,为了避免潜在的错误,我们应该尽量使用 const
, enum
, inline
代替 #define
enum class OutputFormat
{
Markdown,
Html
};
struct TextProcessor
{
void clear()
{
// 清空oss
oss.str("");
// 复位oss
oss.clear();
}
// 将文本转换为列表格式文本
void append_list(const vector<string> items)
{
list_strategy->start(oss);
for (auto& item : items)
list_strategy->add_list_item(oss, item);
list_strategy->end(oss);
}
void set_output_format(const OutputFormat format)
{
switch(format)
{
case OutputFormat::Markdown:
list_strategy = make_unique<MarkdownListStrategy>();
break;
case OutputFormat::Html:
list_strategy = make_unique<HtmlListStrategy>();
break;
default:
throw runtime_error("Unsupported strategy.");
}
}
string str() const { return oss.str(); }
private:
// 存放输出
ostringstream oss;
// 策略类型
unique_ptr<ListStrategy> list_strategy;
};
需要注意上述代码中的 oss.str("")
是实现意义上的清空操作,而 oss.clear()
是对 stream内部对 error state flag 进行置位操作
// 检验
// markdown
TextProcessor tp;
tp.set_output_format(OutputFormat::Markdown);
tp.append_list({"foo", "bar", "baz"});
cout << tp.str() << endl;
// * foo
// * bar
// * baz
// html
tp.clear();
tp.set_output_format(OutputFormat::Html);
tp.append_list({"foo", "bar", "baz"});
cout << tp.str() << endl;
// <ul>
// <li>foo</li>
// <li>bar</li>
// <li>baz</li>
// </ul>
静态策略
静态策略的实现依赖于c++一个强大的功能-模板,在定义处理文件流的组件时,只需要使用简单的模板替换即可获得下面的代码
template <typename LS>
struct TextProcessor
{
void clear()
{
oss.str("");
oss.clear();
}
void append_list(const vector<string> items)
{
list_strategy.start(oss);
for (auto& item : items)
list_strategy.add_list_item(oss, item);
list_strategy.end(oss);
}
string str() const { return oss.str(); }
private:
ostringstream oss;
LS list_strategy;
};
基于模板的静态策略不支持像前文动态策略那样的在运行时切换策略,为了实现不同的策略,我们必须创建基于不同模板的 TextProcessor
实例
// markdown
TextProcessor<MarkdownListStrategy> tpm;
tpm.append_list({"foo", "bar", "baz"});
cout << tpm.str() << endl;
// * foo
// * bar
// * baz
// html
TextProcessor<HtmlListStrategy> tph;
tph.append_list({"foo", "bar", "baz"});
cout << tph.str() << endl;
// <ul>
// <li>foo</li>
// <li>bar</li>
// <li>baz</li>
// </ul>
参考资料
- 《Design Patterns in Modern C++》 Dmitri Nesteruk
- 《Effective C++ 改善程序与设计的55个具体做法》 Scott Meyers
- https://cplusplus.com