头图
Strategy mode

Strategy Pattern

Route planning for two points on the map is a typical application scenario of strategy mode. When we plan the route from the start to the end, we expect the map to give the best route for these ways: walking. Bus, driving. Sometimes it may be subdivided into several strategies such as public transport (rail transit priority) and public transport (transfer priority).

Standard implementation

According to our construction habits, the following is a framework code for path planning

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);
}

In addition to the writing method in the above test code part, we can also invoke the factory pattern to create all router instances, and enumerate all routers to get all path plans at once. This way of traversal is also a solution that will actually be used in engineering. For example, the map software always manages all possible routers in this way.

dp-strategy

tidy

Based on the above example, we can rearrange several key points of the strategy pattern:

  1. The strategy mode is to abstract a public interface to complete a specific task from a bunch of methods, and provide a manager and several strategies based on the public interface.
  2. Each strategy represents a different algorithm to achieve a specific task.
  3. The manager doesn't care about the particularity of the specific strategy adopted, as long as it supports a common strategy calculation interface.
  4. The manager is responsible for providing a context to call the strategy calculator.
  5. The context environment can bring different computing environments for strategy computing.
  6. The strategy calculator extracts the parameters of interest from the context according to its algorithm needs to complete the calculation
  7. The calculation result is abstracted into the form of a public class.
  8. The strategy calculator can derive its own special implementation based on the public result class, but in order for the manager to extract the result, a public interface for extracting the result must be agreed at this time.

In the sample code, an intermediate layer router_t<T> template class is provided. It is here that we intend to introduce a factory creation and registration mechanism for routers, so that the only instance of all routers can be collected in a manager, which can be later in router_guide Use them.

Policy-based Programming

I have talked about policy-oriented metaprogramming techniques C++ policies & traits

Policy-oriented programming has some similarities and differences with the policy pattern.

For example, choose different pen type writers:

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();
}

This is an example of a policy pattern implemented with Policy-oriented programming techniques. It has such nuances worth noting:

  1. There is no common base class for strategy

    Unlike the router base class in the previous article to provide a strategy operation interface, in the world of meta-programming, you can directly do interface coupling with the help of SFINAE techniques.

  2. Due to the characteristics of template expansion during compile time, dynamic switching strategy during runtime becomes relatively infeasible.

    In order to achieve dynamic switching during runtime, you may need to attach some codes to provide several writers, which respectively expand different pen types for use during runtime.

Possibly appropriate place

According to the usual understanding, you might think that the former method is the standard strategy model. Moreover, why would there be any scenarios that require me to choose a strategy to solidify at compile time?

In fact, it is true.

The pen type is chosen as an example above, just to demonstrate the code writing method more concisely (and it is the case in our previous article), but it is really not the best example of compile-time strategy selection.

But imagine this situation, you are a class library author, are providing a general-purpose socket communication library. Then for blocking and non-blocking, two policy classes can be provided to implement them separately. The user can choose the most appropriate policy according to his communication scenario when using your socket library:

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(){
    // ...
  }
};

In this configuration, by choosing the communication mode as blocking or non-blocking, the strategy mode will also be implemented, but it is suitable for the choice made during compile time.

Similarly, when using this communication library, you can also choose whether to use TCP or UDP communication, which algorithm is used for encoding and decoding of datagrams, etc. These can all be done through the template parameter of socket_man in the way of policy to complete the selection of the strategy mode.

postscript

Two typical implementation methods are provided in the article, which respectively represent the compile-time deployment and run-time deployment schemes of the strategy mode.

You can refer to and choose one according to the actual scene.

The engineering application of the strategy mode can also take many forms, and it does not even have to be limited to the coding level of a specific language. For example, we can also provide binary-level strategy selection through the plug-in structure.

Another: This time I didn't use the proprietary features of C++17. However, we need to serialize this title.


hedzr
95 声望19 粉丝