1

谈 C++17 里的 Strategy 模式

 2 years ago
source link: https://segmentfault.com/a/1190000040767611
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

策略模式

Strategy Pattern

在地图上对两点进行路线规划就是一种典型的策略模式应用场景。当我们进行起点到终点的路线规划时,我们期待地图给出这些方式的最佳路线:步行。公交,驾车。有时候可能细分为公交(轨道交通优先),公交(换乘优先)等若干策略。

按照我们的构造习惯,下面是路径规划的一个框架性代码

namespace hicc::dp::strategy::basic {

    struct guide {};
    struct context {};

    class router {
    public:
        virtual ~router() {}
        virtual guide make_guide(context &ctx) = 0;
    };

    template<typename T>
    class router_t : public router {
    public:
        virtual ~router_t() {}
    };

    class by_walk : public router_t<by_walk> {
    public:
        virtual ~by_walk() {}
        guide make_guide(context &ctx) override {
            guide g;
            UNUSED(ctx);
            return g;
        }
    };

    class by_transit : public router_t<by_transit> {
    public:
        virtual ~by_transit() {}
        guide make_guide(context &ctx) override {
            guide g;
            UNUSED(ctx);
            return g;
        }
    };

    class by_drive : public router_t<by_drive> {
    public:
        virtual ~by_drive() {}
        guide make_guide(context &ctx) override {
            guide g;
            UNUSED(ctx);
            return g;
        }
    };

    class route_guide {
    public:
        void guide_it(router &strategy) {
            context ctx;
            guide g = strategy.make_guide(ctx);
            print(g);
        }

    protected:
        void print(guide &g) {
            UNUSED(g);
        }
    };

} // namespace hicc::dp::strategy::basic

void test_strategy_basic() {
    using namespace hicc::dp::strategy::basic;
    route_guide rg;

    by_walk s;
    rg.guide_it(s);
}

除了上面的测试代码部分那样的写法之外,我们还可以援引工厂模式来创建所有 router 的实例,并且枚举全部 routers 来一次性地得到所有路径规划。这种遍历的方式也是工程上真实会采用的方案,例如地图软件中总是这么管理所有的可能的路由器的。

dp-strategy

根据上面的示例,我们可以重新整理出策略模式的若干要点:

  1. 策略模式是从一堆方法中抽象出完成特定任务的公共接口,依据该公共接口,提供一个管理者,以及若干策略。
  2. 每一策略代表着实现特定任务的不同算法。
  3. 管理者不关心具体采用的策略有何特别之处,只要它支持公共的策略计算接口就行。
  4. 管理者负责提供一个上下文环境来调用策略计算器。
  5. 上下文环境能够为策略计算带来不同的计算环境。
  6. 策略计算器根据自己的算法需要从上下文环境中抽取感兴趣的参数,籍以完成计算
  7. 计算结果被抽象为一个公共类的形态。
  8. 策略计算器可以在公共结果类的基础上派生出自己的特殊实现,但为了管理者能够抽取结果,此时要约定一个公有的抽取结果的接口。

在示例代码中提供了一个模板类的中间层 router_t<T>,我们正是打算在这个位置为 routers 引入工厂创建和注册机制,以便能够在一个管理器中收集全部 routers 的唯一实例,稍后可以在 router_guide 中使用它们。

Policy-based Programming

此前我在 C++ policies & traits 中曾经谈论过面向 policy 的元编程手法。

面向 Policy 编程,和策略模式有些相似之处,也有不同之处。

例如选择不同笔型的写作器:

struct InkPen {
    void Write() {
        this->WriteImplementation();
    }

    void WriteImplementation() {
        std::cout << "Writing using a inkpen" << std::endl;
    }
};

struct BoldPen {
    void Write() {
        std::cout << "Writing using a boldpen" << std::endl;
    }
};

template<class PenPolicy>
class Writer : private PenPolicy {
public:
    void StartWriting() {
        PenPolicy::Write();
    }
};

void test_policy_1() {
    Writer<InkPen> writer;
    writer.StartWriting();
    Writer<BoldPen> writer1;
    writer1.StartWriting();
}

这是用面向 Policy 编程手法实作的策略模式的一个例子。它有这样的细微之处值得注意:

  1. 没有 strategy 的公共基类了

    不像前文的 router 的基类来提供一个策略操作接口,元编程的世界里可以借助于 SFINAE 的技巧直接做接口耦合。

  2. 由于模板的编译期展开的特点,因此在运行期动态切换策略变得较为不可行。

    为了做到运行期动态切换,你可能需要特别附加若干代码来提供数个 writers,它们分别对不同的笔型进行展开,以备运行期可用。

可能恰当的场所

按照通常的理解,你或许会觉得前一种方法才是标准的策略模式。而且,怎么会有什么场景需要我选择一种策略在编译期就固化下来呢?

事实上还真是有。

在上面选择笔型作为示例,只是为了较为精简地演示出代码编写方法(而且它是我们上一篇文章中的案例),但它确实不是编译期策略选择的最佳例子。

但是设想这样的情况,你是一个类库作者,正在提供一个通用型的 socket 通信库。那么对于阻塞式和非阻塞式就可以提供两个 policy class 分别予以实现。而用户在使用你的 socket 库时可以根据他的通信场景选择一个最恰当的 policy:

class non_blocked {
  public:
  void connect(){}
  void disconnect(){}
  
};

class blocked {
  public:
  void connect(){}
  void disconnect(){}
};

struct tcp_conn;
struct udp_conn;

template<typename DiagramMode = tcp_conn,
         typename CommunicateMode = non_blocked,
         typename Codec = packaged_codec>
class socket_man {
  public:
  void connect(){}
  void disconnect(){}
  
  protected:
  virtual void on_connect(...);
  virtual void on_recv(...);
  virtual void on_send(...);
  
  protected:
  static void runner_routine(){
    // ...
  }
};

在这个构型中,通过选择通信模式为阻塞或非阻塞,同样也将实现了策略模式,但它就是适合于编译期做出的选择。

类似的,你还可以在使用这个通信库时选择时 TCP 还是 UDP 通信,数据报的编码解码采用何种算法等等,这些都可以通过 socket_man 的模板参数以 policy 的方式完成策略模式的选择。

文中提供了两种典型的实作方法,分别代表着策略模式的编译期展开和运行期展开方案。

根据实际的场景你可以参考并挑选一种。

策略模式的工程上的应用还可以有很多种形态,甚至不必局限于编码、特定语言的编码层面中。例如我们还可以通过插件结构的方式来提供二进制层面的策略选择。

另:这一次倒是没用到 C++17 的专有特性。奈何咱们这标题需要系列化嘛。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK