47、适配器与策略模式:设计模式的对比与应用

适配器与策略模式:设计模式的对比与应用

在软件开发中,我们常常会遇到需要创建自定义值类型并控制其操作集的问题。针对这个问题,有两种常见的解决方案:适配器模式和基于策略的设计模式。下面将详细介绍这两种模式,并对它们进行对比分析。

适配器模式的实现与问题

适配器模式允许我们进行反向转换。例如,通过以下代码可以实现从自定义类型到基础类型的转换:

explicit ImplicitTo(value_type v) : base_t(v) {}
operator basic_type(){ return this->val_; }
operator const basic_type() const { return this->val_; }

使用示例如下:

using V = ImplicitTo<Ordered<Addable<Value<int>>>>;
void f(int i);
V i(3);
f(i);

这种设计虽然能够完成任务,但编写适配器本身较为复杂。递归应用 CRTP(Curiously Recurring Template Pattern)会让人在理解和编写代码时感到困惑,直到习惯这种模板适配器的思维方式。

基于策略的设计模式

传统的基于策略的设计模式通过以下方式实现自定义值类型:

template <typename T, typename AdditionPolicy,
          typename ComparisonPolicy,
          typename OrderPolicy,
          typename AssignmentPolicy, ... >
class Value { ... };

然而,这种实现方式存在诸多缺点:
- 策略列表冗长 :所有策略都必须明确列出,且没有良好的默认值。
- 策略位置敏感 :类型声明需要仔细计算逗号的数量,随着新策略的添加,策略的有意义顺序会消失。

但在某些情况下,不同策略集创建不同类型是设计意图。例如,我们希望有支持加法的类型和不支持加法的类似类型,它们应该是不同的类型。

改进的策略设计

我们希望能够只列出我们希望值类型具有的属性的策略。为了实现这一点,我们可以使用以下方法:
首先,思考策略的样子。例如,启用加法的策略应该在类的公共接口中注入 operator+() (可能还有 operator+=() ),使值可赋值的策略应该注入 operator= 。这些策略通常是基类,使用 CRTP 来知道派生类的类型。

以下是一个 Incrementable 策略的示例:

template <
    typename T,      // 基础类型 (如 int)
    typename V>      // 派生类
struct Incrementable {
    V operator++() {
        V& v = static_cast<V&>(*this);
        ++v.val_;      // 派生类中的值
        return v;
    }
};

为了支持任意数量和顺序的策略,我们使用可变参数模板:

template <typename T,
          template <typename, typename> class ... Policies>
class Value :
    public Policies<T, Value<T, Policies ... >> ...
{ ... };

Value 类模板应该包含我们希望所有值类型共有的接口,其余部分由策略提供。默认情况下,我们让值类型可复制、可赋值和可打印:

template <typename T,
          template <typename, typename> class ... Policies>
class Value :
    public Policies<T, Value<T, Policies ... >> ...
{
    public:
    using base_type = T;
    explicit Value() = default;
    explicit Value(T v) : val_(v) {}
    Value(const Value& rhs) : val_(rhs.val_) {}
    Value& operator=(Value rhs) {
        val_ = rhs.val_;
        return *this;
    }
    Value& operator=(T rhs) { val_ = rhs; return *this; }
    friend std::ostream&
    operator<<(std::ostream& out, Value x) {
        out << x.val_; return out;
    }
    friend std::istream&
        operator>>(std::istream& in, Value& x) {
        in >> x.val_; return in;
    }
    private:
    T val_ {};
};

由于 val_ 是私有的,而策略需要访问和修改它,我们提供了访问器函数:

template <typename T,
          template <typename, typename> class ... Policies>
class Value :
    public Policies<T, Value<T, Policies ... >> ...
{
    public:
    ...
    T get() const { return val_; }
    T& get() { return val_; }
    private:
    T val_ {};
};

要创建具有受限操作集的值类型,我们只需实例化模板并列出所需的策略:

using V = Value<int, Addable, Incrementable>;
V v1(0), v2(1);
v1++; // Incrementable - OK
V v3(v1 + v2); // Addable - OK
v3 *= 2; // No multiplication policies - won't compile
常见策略示例

以下是一些常见策略的示例:
- Incrementable 策略 :提供前缀和后缀 ++ 运算符。

template <typename T, typename V> struct Incrementable {
    V operator++() {
        V& v = static_cast<V&>(*this);
        ++(v.get());
        return v;
    }
    V operator++(int) {
        V& v = static_cast<V&>(*this);
        return V(v.get()++);
    }
};
  • 支持 += 运算符的 Incrementable 策略
template <typename T, typename V> struct Incrementable {
    V& operator+=(V val) {
        V& v = static_cast<V&>(*this);
        v.get() += val.get();
        return v;
    }
    V& operator+=(T val) {
        V& v = static_cast<V&>(*this);
        v.get() += val;
        return v;
    }
};
  • 比较策略
template <typename T, typename V> struct ComparableSelf {
    friend bool operator==(V lhs, V rhs) {
        return lhs.get() == rhs.get();
    }
    friend bool operator!=(V lhs, V rhs) {
        return lhs.get() != rhs.get();
    }
};

template <typename T, typename V> struct ComparableValue {
    friend bool operator==(V lhs, T rhs) {
        return lhs.get() == rhs;
    }
    friend bool operator==(T lhs, V rhs) {
        return lhs == rhs.get();
    }
    friend bool operator!=(V lhs, T rhs) {
        return lhs.get() != rhs;
    }
    friend bool operator!=(T lhs, V rhs) {
        return lhs != rhs.get();
    }
};

template <typename T, typename V>
struct Comparable : public ComparableSelf<T, V>,
                    public ComparableValue<T, V> {};
  • 加法策略
template <typename T, typename V> struct Addable {
    friend V operator+(V lhs, V rhs) {
        return V(lhs.get() + rhs.get());
    }
    friend V operator+(V lhs, T rhs) {
        return V(lhs.get() + rhs);
    }
    friend V operator+(T lhs, V rhs) {
        return V(lhs + rhs.get());
    }
};
  • 显式转换策略
template <typename T, typename V>
struct ExplicitConvertible {
    explicit operator T() {
        return static_cast<V*>(this)->get();
    }
    explicit operator const T() const {
        return static_cast<const V*>(this)->get();
    }
};
改进策略设计的局限性

这种改进的策略设计模式虽然解决了传统策略设计的一些问题,但也存在两个基本限制:
- 无法按名称引用策略 :基于策略的类不能按名称引用任何策略,没有约定强制的策略接口,绑定策略到单个类型的过程是隐式的。
- 没有默认策略 :缺少的策略就是缺失的,没有任何替代。默认行为总是没有任何行为。

例如,我们不能使用 enable_if 技术来实现默认行为,因为所有策略槽都是未命名的。

两种模式的对比
模式 优点 缺点
适配器模式 - 可以解决广泛的设计挑战
- 可用于运行时和编译时接口转换
- 编写适配器复杂
- 递归应用 CRTP 难以理解
改进的策略模式 - 策略顺序无关紧要
- 可只指定所需策略
- 无法按名称引用策略
- 没有默认策略

综上所述,适配器模式和基于策略的设计模式各有优缺点。在实际应用中,我们需要根据具体问题和需求来选择合适的模式。当策略主要用于控制一组支持的操作时,改进的策略模式可能是一个有吸引力的选择;而当需要解决更广泛的设计挑战时,适配器模式可能更合适。

在软件开发中,面对复杂问题时,往往有多种设计方案可供选择。通过了解不同模式的优缺点,我们可以更好地评估和选择适合实际问题的设计方案。

适配器与策略模式:设计模式的对比与应用

策略模式的应用场景分析

策略模式在某些特定场景下具有独特的优势。当我们需要对一个值类型进行灵活的功能组合时,策略模式可以让我们轻松地实现这一点。例如,在一个数值计算系统中,我们可能需要对不同类型的数值进行不同的操作,如加法、减法、乘法等。通过策略模式,我们可以根据需要选择不同的策略来实现这些操作。

下面是一个简单的流程图,展示了如何使用策略模式来创建一个支持不同操作的数值类型:

graph TD;
    A[定义基础类型 T] --> B[定义策略模板 Policies];
    B --> C[创建 Value 类模板,继承策略];
    C --> D[实例化 Value 类,指定所需策略];
    D --> E[使用实例进行操作];

在这个流程图中,我们首先定义了基础类型 T ,然后定义了各种策略模板 Policies 。接着,我们创建了 Value 类模板,它继承了这些策略。最后,我们实例化 Value 类,并指定所需的策略,就可以使用这个实例进行相应的操作了。

适配器模式的深入理解

适配器模式的核心思想是将一个类的接口转换成客户希望的另一个接口。在前面的例子中,我们看到了如何使用适配器模式进行类型转换。适配器模式还可以用于解决不同组件之间的兼容性问题。

例如,假设我们有一个旧的组件,它提供了一种特定的接口,而我们的新系统需要使用另一种接口。这时,我们可以创建一个适配器类,将旧组件的接口转换为新系统所需的接口。

以下是一个简单的适配器模式示例:

// 旧组件接口
class OldComponent {
public:
    void oldOperation() {
        // 旧操作的实现
    }
};

// 新系统所需接口
class NewInterface {
public:
    virtual void newOperation() = 0;
};

// 适配器类
class Adapter : public NewInterface {
private:
    OldComponent oldComponent;
public:
    void newOperation() override {
        oldComponent.oldOperation();
    }
};

在这个示例中, OldComponent 是旧组件,它提供了 oldOperation 方法。 NewInterface 是新系统所需的接口,它定义了 newOperation 方法。 Adapter 类是适配器类,它继承了 NewInterface 接口,并在 newOperation 方法中调用了 OldComponent oldOperation 方法,从而实现了接口的转换。

两种模式的选择建议

在选择适配器模式还是策略模式时,我们可以考虑以下几个因素:
1. 问题的复杂性 :如果问题比较简单,只需要进行一些接口转换或功能组合,那么策略模式可能更合适;如果问题比较复杂,涉及到不同组件之间的兼容性问题,那么适配器模式可能更合适。
2. 灵活性需求 :如果需要对功能进行灵活的组合和扩展,那么策略模式可以提供更好的灵活性;如果需要对接口进行固定的转换,那么适配器模式更能满足需求。
3. 代码的可维护性 :策略模式的代码通常比较简洁,易于理解和维护;而适配器模式可能需要编写更多的代码来实现接口的转换,代码的复杂度相对较高。

以下是一个简单的表格,总结了两种模式的选择建议:
| 因素 | 适配器模式 | 策略模式 |
| ---- | ---- | ---- |
| 问题复杂性 | 适用于复杂问题,如组件兼容性问题 | 适用于简单问题,如功能组合 |
| 灵活性需求 | 接口转换固定,灵活性较低 | 功能组合灵活,可按需扩展 |
| 代码可维护性 | 代码复杂度较高,维护难度较大 | 代码简洁,易于理解和维护 |

总结与展望

适配器模式和策略模式是软件开发中常用的两种设计模式,它们各有优缺点。适配器模式可以解决广泛的设计挑战,包括接口转换和组件兼容性问题;而策略模式则可以让我们灵活地组合和扩展功能。

在实际应用中,我们需要根据具体问题和需求来选择合适的模式。同时,我们也可以将两种模式结合使用,以充分发挥它们的优势。例如,在一个大型系统中,我们可以使用适配器模式来解决不同组件之间的兼容性问题,同时使用策略模式来对组件的功能进行灵活的组合和扩展。

未来,随着软件开发技术的不断发展,设计模式也将不断演变和完善。我们需要不断学习和掌握新的设计模式,以应对日益复杂的软件开发需求。同时,我们也需要根据实际情况灵活运用设计模式,而不是生搬硬套。只有这样,我们才能设计出高质量、可维护的软件系统。

常见问题解答

以下是一些关于适配器模式和策略模式的常见问题解答:
1. 适配器模式和策略模式有什么本质区别?
- 适配器模式主要用于解决接口不兼容的问题,将一个类的接口转换成另一个类的接口;而策略模式主要用于实现算法的可替换性,通过不同的策略来实现不同的行为。
2. 在什么情况下应该优先选择适配器模式?
- 当需要将一个现有的类或组件集成到一个新的系统中,而它们的接口不兼容时,应该优先选择适配器模式。
3. 策略模式可以替代适配器模式吗?
- 策略模式和适配器模式解决的是不同的问题,不能完全替代。但在某些情况下,策略模式可以提供类似的功能,具体取决于问题的性质。
4. 如何确保策略模式的代码可维护性?
- 可以通过合理设计策略接口、使用清晰的命名和注释、遵循单一职责原则等方式来确保策略模式的代码可维护性。

通过对适配器模式和策略模式的深入学习和理解,我们可以更好地应对软件开发中的各种挑战,设计出更加优秀的软件系统。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值