46、适配器与策略模式:自定义值类型设计

适配器与策略模式:自定义值类型设计

1. 容器打印与适配器的使用

在处理容器打印时,我们原本以为 operator<<() 可以打印任何容器,但实际上并非如此。像 std::array 这种具有一个类型和一个非类型参数的类模板容器,就无法直接用之前的 operator<<() 打印。我们可以通过声明一个重载函数来处理这种情况:

// Example 23
template <typename T,
          template <typename, size_t> class Container, size_t N>
std::ostream& operator<<(std::ostream& out,
                         const Container<T, N>& c) {
    ...
}

然而,可能还存在其他类型的容器不适合这些模板,这时就需要再次使用适配器。

2. 适配器与策略模式概述

适配器和策略(或策略)模式是较为通用的模式,C++ 为这些模式添加了泛型编程能力,这不仅扩展了它们的可用性,有时还会模糊这些模式之间的界限。策略模式提供自定义实现,而适配器模式则改变接口并为现有接口添加功能。虽然这两种模式不同,但它们可解决的问题类型有很大重叠。接下来,我们将通过设计自定义值类型的问题来比较这两种方法。

3. 自定义值类型需求

值类型通常表现得像 int ,常见的是各种数字类型。我们可能需要对有理数、复数、张量、矩阵或带有单位的数字进行操作。这些值类型支持一系列操作,如算术运算、比较、赋值和复制。根据值的具体含义,我们可能只需要这些操作的一个子集。例如,矩阵可能只需要支持加法和乘法,而不需要除法,并且在大多数情况下,比较矩阵是否相等以外的操作可能没有意义。我们希望设计一种具有有限接口的数值类型,使不允许的操作无法编译。

4. 适配器解决方案

4.1 初始值类型

我们从一个基本的值类型开始,它的接口几乎不支持任何操作,然后逐步添加所需的功能。以下是初始的 Value 类模板:

// Example 24
template <typename T> class Value {
public:
    using basic_type = T;
    using value_type = Value;
    explicit Value() : val_(T()) {}
    explicit Value(T v) : val_(v) {}
    Value(const Value&) = default;
    Value& operator=(const Value&) = default;
    Value& operator=(basic_type rhs) {
        val_ = rhs;
        return *this;
    }
protected:
    T val_ {};
};

为了方便,我们让 Value 类型可打印:

// Example 24
template <typename T> class Value {
public:
    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;
    }
    ...
};

目前,我们只能对 Value 类型进行初始化、赋值和打印操作:

// Example 24
using V = Value<int>;
V i, j(5), k(3);
i = j;
std::cout << i;  // Prints 5

4.2 添加比较接口

我们可以创建一个适配器来添加比较接口:

// Example 24
template <typename V> class Comparable : public V {
public:
    using V::V;
    using V::operator=;
    using value_type = typename V::value_type;
    using basic_type = typename value_type::basic_type;
    Comparable(value_type v) : V(v) {}
    friend bool operator==(Comparable lhs, Comparable rhs) {
        return lhs.val_ == rhs.val_;
    }
    friend bool operator==(Comparable lhs, basic_type rhs) {
        return lhs.val_ == rhs;
    }
    friend bool operator==(basic_type lhs, Comparable rhs) {
        return lhs == rhs.val_;
    }
    ... same for the operator!= ...
};

使用示例:

using V = Comparable<Value<int>>;
V i(3), j(5);
i == j; // False
i == 3; // True
5 == j; // Also true

4.3 添加顺序接口

Ordered 适配器可以提供 < <= > >= 运算符:

// Example 24
template <typename V> class Ordered : public V {
public:
    using V::V;
    using V::operator=;
    using value_type = typename V::value_type;
    using basic_type = typename value_type::basic_type;
    Ordered(value_type v) : V(v) {}
    friend bool operator<(Ordered lhs, Ordered rhs) {
        return lhs.val_ < rhs.val_;
    }
    friend bool operator<(basic_type lhs, Ordered rhs) {
        return lhs < rhs.val_;
    }
    friend bool operator<(Ordered lhs, basic_type rhs) {
        return lhs.val_ < rhs;
    }
    ... same for the other operators ...
};

我们可以将 Comparable Ordered 适配器组合使用:

using V = Ordered<Comparable<Value<int>>>;
// Or Comparable<Ordered<...>
V i(3), j(5);
i == j; // False
i <= 3; // True

4.4 添加算术接口

对于数值类型,我们可能需要添加算术运算,如加法和减法。以下是 Addable 装饰器:

// Example 24
template <typename V> class Addable : public V {
public:
    using V::V;
    using V::operator=;
    using value_type = typename V::value_type;
    using basic_type = typename value_type::basic_type;
    Addable(value_type v) : V(v) {}
    friend Addable operator+(Addable lhs, Addable rhs) {
        return Addable(lhs.val_ + rhs.val_);
    }
    friend Addable operator+(Addable lhs, basic_type rhs) {
        return Addable(lhs.val_ + rhs);
    }
    friend Addable operator+(basic_type lhs, Addable rhs) {
        return Addable(lhs + rhs.val_);
    }
    ... same for the operator- ...
};

使用示例:

using V = Addable<Value<int>>;
V i(5), j(3), k(7);
k = i + j; // 8

我们也可以将 Addable 与其他装饰器组合使用:

using V = Addable<Ordered<Value<int>>>;
V i(5), j(3), k(7);
if (k - 1 < i + j) { ... yes it is ... }

4.5 组合装饰器的问题与解决方法

当我们尝试不同的装饰器组合时,会遇到组合性问题。例如:

using V = Ordered<Addable<Value<int>>>;
V i(5), j(3), k(7);
if (k - 1 < i + j) { ... }

这里会出现编译错误,因为 i + j 返回的是 Addable<Value<int>> 类型,而比较运算符期望的是 Ordered<Addable<Value<int>>> 类型。为了解决这个问题,我们需要让每个返回装饰类型的运算符返回原始(最外层)类型。这可以通过引入模板重绑定(template rebinding)和奇特递归模板模式(CRTP)来实现。

每个装饰器将有两个模板参数,第一个是链中的下一个装饰器,第二个是最外层类型(最终值类型)。以下是修改后的 Ordered 装饰器:

// Example 25
template <typename V, typename FV = void>
class Ordered : public V::template rebind<FV> {
    using base_t = typename V::template rebind<FV>;
public:
    using base_t::base_t;
    using base_t::operator=;
    template <typename FV1> using rebind = Ordered<V, FV1>;
    using value_type = typename base_t::value_type;
    using basic_type = typename value_type::basic_type;
    explicit Ordered(value_type v) : base_t(v) {}
    friend bool operator<(FV lhs, FV rhs) {
        return lhs.val_ < rhs.val_;
    }
    ... the rest of the operators ...
};

template <typename V> class Ordered<V, void>
    : public V::template rebind<Ordered<V>> {
    using base_t = typename V::template rebind<Ordered>;
public:
    using base_t::base_t;
    using base_t::operator=;
    template <typename FV1> using rebind = Ordered<V, FV1>;
    using value_type = typename base_t::value_type;
    using basic_type = typename value_type::basic_type;
    explicit Ordered(value_type v) : base_t(v) {}
    friend bool operator<(Ordered lhs, Ordered rhs) {
        return lhs.val_ < rhs.val_;
    }
    ... the rest of the operators ...
};

对于返回值的装饰器,如 Addable ,也需要进行相应修改:

// Example 25
template <typename V, typename FV = void> class Addable :
    public V::template rebind<FV> {
    friend FV operator+(FV lhs, FV rhs) {
        return FV(lhs.val_ + rhs.val_);
    }
    ...
};

template <typename V> class Addable<V, void> :
    public V::template rebind<FV> {
    friend Addable operator+(Addable lhs,Addable rhs) {
        return Addable(lhs.val_ + rhs.val_);
    }
    ...
};

现在,我们可以以任意顺序组合装饰器,并定义具有任意操作子集的值类型:

// Example 25
using V = Comparable<Ordered<Addable<Value<int>>>>;
// Addable<Ordered<Comparable<Value<int>>>> also OK
V i, j(5), k(3);
i = j; j = 1;
i == j;          // OK – Comparable
i > j;           // OK – Ordered
i + j == 7 – k;  // OK – Comparable and Addable
i*j;             // Not Multipliable – does not compile

4.6 添加成员函数和构造函数

除了添加运算符,我们还可以添加成员函数和构造函数。例如,添加隐式转换构造函数的 ImplicitFrom 装饰器:

// Example 25
template <typename V, typename FV = void>
class ImplicitFrom : public V::template rebind<FV> {
    ...
    explicit ImplicitFrom(value_type v) : base_t(v) {}
    ImplicitFrom(basic_type rhs) : base_t(rhs) {}
};

template <typename V> class ImplicitFrom<V, void> :
    public V::template rebind<ImplicitFrom<V>> {
    ...
    explicit ImplicitFrom(value_type v) : base_t(v) {}
    ImplicitFrom(basic_type rhs) : base_t(rhs) {}
};

使用示例:

using V = ImplicitFrom<Ordered<Addable<Value<int>>>>;
void f(V v);
f(3);

如果需要向底层类型进行隐式转换,可以使用类似的适配器,但添加的是转换运算符。

5. 总结

通过适配器模式,我们可以逐步为基本值类型添加所需的功能。然而,在组合装饰器时,需要注意返回类型的问题,通过模板重绑定和 CRTP 可以解决这个问题。此外,我们还可以通过添加成员函数和构造函数来进一步扩展值类型的功能。

以下是一个简单的流程图,展示了适配器模式添加功能的过程:

graph LR
    A[Value类模板] --> B[Comparable适配器]
    A --> C[Ordered适配器]
    A --> D[Addable适配器]
    B --> E[组合使用]
    C --> E
    D --> E

以下是不同装饰器及其功能的表格:
| 装饰器 | 功能 |
| ---- | ---- |
| Comparable | 添加比较运算符(==, !=) |
| Ordered | 添加顺序运算符(<, <=, >, >=) |
| Addable | 添加加法和减法运算符 |
| ImplicitFrom | 添加从底层类型的隐式转换构造函数 |
| ImplicitTo | 添加向底层类型的隐式转换运算符 |

6. 策略模式解决方案

6.1 策略模式概述

策略模式允许我们通过插入不同的策略来配置类型的一组功能。与适配器模式不同,策略模式更侧重于提供自定义实现,而不是改变接口。在设计自定义值类型时,我们可以使用策略模式来控制值类型支持的操作。

6.2 定义策略类

我们可以为不同的操作定义策略类。例如,为比较操作定义一个 ComparisonPolicy 策略类:

// ComparisonPolicy 策略类
template <typename T>
struct ComparisonPolicy {
    static bool equal(const T& lhs, const T& rhs) {
        return lhs == rhs;
    }
    static bool less(const T& lhs, const T& rhs) {
        return lhs < rhs;
    }
    // 其他比较操作...
};

为算术操作定义一个 ArithmeticPolicy 策略类:

// ArithmeticPolicy 策略类
template <typename T>
struct ArithmeticPolicy {
    static T add(const T& lhs, const T& rhs) {
        return lhs + rhs;
    }
    static T subtract(const T& lhs, const T& rhs) {
        return lhs - rhs;
    }
    // 其他算术操作...
};

6.3 使用策略类创建值类型

我们可以使用这些策略类来创建一个自定义值类型:

// 使用策略类的 Value 类模板
template <typename T, 
          typename Comparison = ComparisonPolicy<T>,
          typename Arithmetic = ArithmeticPolicy<T>>
class Value {
private:
    T value;
public:
    Value(const T& val) : value(val) {}

    // 使用比较策略
    bool isEqual(const Value& other) const {
        return Comparison::equal(value, other.value);
    }
    bool isLess(const Value& other) const {
        return Comparison::less(value, other.value);
    }

    // 使用算术策略
    Value add(const Value& other) const {
        return Value(Arithmetic::add(value, other.value));
    }
    Value subtract(const Value& other) const {
        return Value(Arithmetic::subtract(value, other.value));
    }

    // 获取值
    T getValue() const {
        return value;
    }
};

使用示例:

// 使用策略类创建值类型
using MyValue = Value<int>;
MyValue a(5);
MyValue b(3);

bool equal = a.isEqual(b); // false
bool less = a.isLess(b);   // false
MyValue sum = a.add(b);    // 8

6.4 策略模式的优势

  • 灵活性 :可以轻松替换不同的策略类,以改变值类型的行为。例如,如果需要不同的比较规则,可以定义一个新的 ComparisonPolicy 类并替换原有的策略。
  • 可维护性 :策略类的实现与值类型的实现分离,使得代码更易于维护和扩展。

6.5 策略模式的局限性

  • 接口一致性 :策略类需要遵循一定的接口规范,否则可能会导致使用时的错误。
  • 策略组合复杂性 :当需要组合多个策略时,可能会增加代码的复杂性。

7. 适配器模式与策略模式的比较

7.1 功能侧重点

模式 功能侧重点
适配器模式 改变接口,为现有接口添加功能
策略模式 提供自定义实现,控制类型的功能配置

7.2 代码复杂度

  • 适配器模式 :在组合多个适配器时,需要处理返回类型的问题,可能会增加代码的复杂度。但通过模板重绑定和 CRTP 可以解决这些问题。
  • 策略模式 :策略类的实现相对独立,但在组合多个策略时,需要确保策略类之间的兼容性,也会增加一定的复杂度。

7.3 可维护性

  • 适配器模式 :适配器类与被装饰的类紧密相关,修改适配器可能会影响到被装饰的类。但可以通过合理的设计来降低这种影响。
  • 策略模式 :策略类与值类型分离,修改策略类不会影响到值类型的其他部分,可维护性较好。

7.4 灵活性

  • 适配器模式 :可以在运行时动态地添加或移除功能,但组合时需要注意顺序和返回类型。
  • 策略模式 :可以在编译时通过选择不同的策略类来配置类型的功能,灵活性较高。

以下是适配器模式和策略模式的比较流程图:

graph LR
    A[适配器模式] --> B[改变接口]
    A --> C[添加功能]
    A --> D[组合时需处理返回类型]
    E[策略模式] --> F[提供自定义实现]
    E --> G[控制功能配置]
    E --> H[组合时需确保策略兼容性]

8. 选择合适的模式

在设计自定义值类型时,选择适配器模式还是策略模式取决于具体的需求:
- 如果需要改变现有接口或为现有接口添加功能 ,可以选择适配器模式。例如,当需要为一个已有的类添加新的操作符时,适配器模式是一个不错的选择。
- 如果需要通过不同的实现来配置类型的功能 ,可以选择策略模式。例如,当需要为一个数值类型提供不同的比较规则或算术运算规则时,策略模式更为合适。

在实际应用中,也可以将两种模式结合使用,以充分发挥它们的优势。例如,先使用策略模式来配置类型的基本功能,再使用适配器模式来为类型添加额外的接口或功能。

9. 总结

本文介绍了适配器模式和策略模式在设计自定义值类型中的应用。适配器模式通过逐步添加功能来扩展基本值类型,但在组合装饰器时需要注意返回类型的问题。策略模式通过插入不同的策略类来配置类型的功能,具有较好的可维护性和灵活性。在选择模式时,需要根据具体的需求来决定使用哪种模式,也可以将两种模式结合使用。

以下是一个简单的决策表格,帮助你选择合适的模式:
| 需求 | 合适的模式 |
| ---- | ---- |
| 改变接口,添加功能 | 适配器模式 |
| 配置类型功能,提供不同实现 | 策略模式 |
| 结合两者优势 | 适配器模式 + 策略模式 |

通过对适配器模式和策略模式的深入理解和应用,我们可以更灵活地设计自定义值类型,满足不同的业务需求。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值