0

This post consists of describing a problem with the straightforward implementation of the policy-based design, proposing an alternative implementation, analyzing the proposed implementation and asking help in giving correct weight to the different factors in the analysis. I apologize for the length of the post and hope that you will stick with me.

PROBLEM DESCRIPTION

Suppose that we use policy-based design as follows:

template <typename FooPolicy> 
struct Alg {
    void operator()() {
        ...
        FooPolicy::foo(arguments);
        ...
    }
};

There is a degree of coupling between the above host class and the policy: if the signature of FooPolicy::foo changes, then the code in Alg::operator() has to change accordingly.

The coupling becomes much tighter if policy classes are given a degree of freedom for choosing an interface. For example, suppose that FooPolicy may implement either foo without parameters or foo with one integer parameter (the implementation for this case was suggested here):

template <typename FooPolicy> struct Alg {
    void operator()() {
        int arg = 5; // any computation can be here
        // using tag dispatch to call the correct `foo`
        foo(std::integral_constant<bool, FooPolicy::paramFlag>{}, arg);
    }

private:
    void foo(std::true_type, int param) { FooPolicy::foo(param); }
    void foo(std::false_type, int param) {
        (void)param;
        FooPolicy::foo();
    }
};

struct SimpleFoo {
    static constexpr bool paramFlag = false;
    static void foo();
};

struct ParamFoo {
    static constexpr bool paramFlag = true;
    static void foo(int param);
};

Obviously, whenever one adds a policy class with a different interface, he will have to update the dispatching mechanism, which can become complicated.

PROPOSED DESIGN

I am considering a design, whereby the algorithm would provide an interface that function members of the policy class can use to get data instead of accepting arguments. For the above example, it might look as follows:

// The policy-independent part of Alg factored out
struct AlgPolicyIndependent {
    int getArg() const { return arg; }
protected:
    int arg;
};

// The interface to be used by FooPolicy
struct FooPolicyServices {
    FooPolicyServices(const AlgPolicyIndependent &myAlg) : alg(myAlg) {};
    int getArg() const { return alg.getArg(); }
private:
    const AlgPolicyIndependent &alg;
};

template <typename FooPolicy>
struct Alg : private AlgPolicyIndependent {
    Alg() : fooPolicy(FooPolicyServices(*this)) {};
    void operator()() {
        arg = 5; // any computation can be here
        fooPolicy.foo();
    }
private:
    FooPolicy fooPolicy;
};

struct SimpleFoo {
    SimpleFoo(const FooPolicyServices &myS) : s(myS) {};
    void foo() { std::cout << "In SimpleFoo" << std::endl; }
private:
    const FooPolicyServices &s;
};

struct ParamFoo {
    ParamFoo(const FooPolicyServices &myS) : s(myS) {};
    void foo() { std::cout << "In ParamFoo " << s.getArg() << std::endl; }
private:
    const FooPolicyServices &s;
};

ANALYSIS

With this design, a policy is free to use any data obtainable by using the public interface of the corresponding Services class. In our example, ParamFoo::foo got the algorithm's arg by using FooPolicyServices::getArg. The host class simply calls FooPolicy::foo without arguments and this will not have to change even if FooPolicy::foo changes, which is the decoupling we wanted.

I see two disadvantages to this design:

  1. arg has become part of the state of Alg instead of being a local variable in Alg::operator(), which goes against Item 26 of Effective C++ saying that variables should be defined as late as possible. However, the reasoning of that item does not apply if the cost of the extra initialization of arg is negligible compared to the cost of running the algorithm.

  2. Policy classes have gotten a state. So, we cannot use policies by simply calling their static member functions.

QUESTIONS

Three questions:

  1. Is the decoupling achieved by the proposed design worth the two disadvantages listed above?

  2. Are there disadvantages that I overlooked?

  3. Does the proposed design have a name?


Based on the reply by @Useless, here is an updated implementation. This implementation makes it possible to have stateless policy classes, but has an overhead of passing the same reference to a Services object each time a policy is used.

// The policy-independent part of Alg
struct AlgPolicyIndependent {
    int getArg() const { return arg; }
protected:
    int arg;
};

// The interface to be used by FooPolicy
struct FooPolicyServices {
    FooPolicyServices(const AlgPolicyIndependent &myAlg) : alg(myAlg) {};
    int getArg() const { return alg.getArg(); }
private:
    const AlgPolicyIndependent &alg;
};

template <typename FooPolicy>
struct Alg : private AlgPolicyIndependent {
    Alg() : fooPolicyServices(*this) {};
    void operator()() {
        arg = 5; // any computation can be here
        FooPolicy::foo(fooPolicyServices);
    }
private:
    FooPolicyServices fooPolicyServices;
};

struct SimpleFoo {
    static void foo(const FooPolicyServices &s) {
        (void)s;
        std::cout << "In SimpleFoo" << std::endl;
    }
};

struct ParamFoo {
    static void foo(const FooPolicyServices &s) {
        std::cout << "In ParamFoo " << s.getArg() << std::endl;
    }
};
4

1 回答 1

1

上述宿主类与策略之间存在一定程度的耦合

或者,“策略模式中固有的耦合是否存在问题”?

不,算法和策略中的每一个与接口之间存在等效程度的耦合,一个需要,另一个实现。

考虑运行时多态等价物,即策略:

struct IStrategy {
    virtual ~IStrategy() {}
    virtual void foo() = 0;
};
struct FooStrategy: public IStrategy {
    void foo() override;
}
void algo(IStrategy *s) {
    // ...
    s->foo();
}

现在,策略模式中(显式基类)接口上的具体策略和算法函数之间的耦合程度与(隐式鸭子类型)接口上的策略和算法模板之间的耦合程度完全相同在策略模式中。

我并不是说这种耦合不存在,但我指出这种耦合程度通常不会被认为是过度的。

请注意,您的变体也有一个精确的运行时类比 - 可选地实现其他(和更具体的)接口,并dynamic_cast用于在运行时进行调查,这是受支持的。


问题

  1. 脱钩……值得上面列出的两个缺点吗?

    也许!这取决于耦合在实践中变得多么麻烦,以及传递了多少状态。

    任何状况之下 ...

  2. 有我忽略的缺点吗?

    一般来说,有状态有很多缺点,尤其是您建议的设计:

    • 全局(即,非本地范围)状态与并发交互不良(使用来自多个线程的相同算法对象并且一切都中断)
    • 它与重入使用的交互很糟糕(意外地其自己的 Policy 调用中使用了相同的算法对象,一切都中断了)
    • 如果你想重用一个有状态的对象,你可能需要提供一个额外的方法来重置它的状态
    • 然后你必须记住在可以重用对象的任何地方调用该方法
    • 上面的两点本质上是一个糟糕的、手动的、容易出错的模拟,首先只使用本地范围状态
    • 除了编写额外的代码之外,您还使您的对象更大,以存储您在方法范围之外并不真正想要的状态
    • 在您的特定情况下,您还使策略更大(以将指针存储回您的状态),并且每个策略调用现在在指针追逐中都有一个额外的间接this->services->value以获取参数

    虽然这些都是问题,但在这里它们也是完全可以避免的。只需在每次调用时将本地范围的上下文对象传递给策略方法。然后:

    • 算法对象根本不需要有状态,因为上下文对象在方法中具有本地范围
    • 也不需要策略是有状态的,因为上下文可以简单地作为每次调用的唯一参数传递

    请注意,在您的编辑中,算法仍然是有状态的。但是,服务/独立的事物(我称之为上下文)可以只是算法方法中的局部变量。无需在该方法之外保留它,并且用本地替换数据成员使算法无状态。

  3. 提议的设计有名称吗?

    对我来说,它看起来像上下文模式。最初的建议看起来像是一种不舒服的自制依赖注入,但我会坚持使用无状态策略和算法,并在 Context 对象中传递工作状态以供偏好。

于 2015-09-17T14:30:51.420 回答