45、C++ 适配器模式详解

C++ 适配器模式详解

C++ 适配器模式详解

1. 适配器模式概述

适配器模式的目的是在不重写对象本身的情况下,将对象的当前接口转换为特定应用所需的接口。不同的应用代码可能对接口有不同的偏好,因此适配器模式有多种设计方式。例如,在多线程环境中, locking_queue 对象的 push() pop() 成员函数由同一个互斥锁保护,使得多个线程可以同时执行这些操作,且行为是明确的,这表明 locking_queue 对象是线程安全的。

2. 函数适配器
  • 柯里化(Currying) :函数适配器的一个常见应用是柯里化函数的一个或多个参数。柯里化意味着我们有一个多参数函数,固定其中一个参数的值,这样在每次调用时就无需再指定该参数。例如,有函数 f(int i, int j) ,我们可以创建一个新函数 g(i) ,它等同于 f(i, 5) ,但无需每次都输入 5
  • 排序函数示例 std::sort 函数通常接受一个迭代器范围(待排序的序列),也可以接受三个参数,第三个参数是比较对象。默认情况下,使用 std::less ,它会调用被排序对象的 operator<() 。现在我们想要模糊比较浮点数,即当两个数 x y 足够接近时,不认为一个小于另一个,只有当 x 远小于 y 时,才强制 x 排在 y 前面。以下是比较仿函数(可调用对象)的示例:
// Example 13
struct much_less {
    template <typename T>
    bool operator()(T x, T y) {
        return x < y && std::abs(x - y) > tolerance;
    }
    static constexpr double tolerance = 0.2;
};

这个比较对象可以与标准排序函数一起使用:

std::vector<double> v;
std::sort(v.begin(), v.end(), much_less());

如果我们经常需要这种排序方式,可以柯里化最后一个参数,创建一个只有两个参数(迭代器)的适配器:

// Example 13
template<typename RandomIt>
void sort_much_less(RandomIt first, RandomIt last) {
    std::sort(first, last, much_less());
}

现在可以用两个参数调用排序函数:

// Example 13
std::vector<double> v;
sort_much_less(v.begin(), v.end());

如果经常以这种方式对整个容器进行排序,还可以再次更改接口,创建另一个适配器:

// Example 14
template<typename Container> 
void sort_much_less(Container& c) {
    std::sort(c.begin(), c.end(), much_less());
}

在 C++20 中, std::sort 和其他 STL 函数有接受范围的变体,这是我们容器适配器的泛化。现在程序代码看起来更简单:

// Example 14
std::vector<double> v;
sort_much_less(v);

C++14 还提供了使用 lambda 表达式编写简单适配器的替代方法,通常更受青睐:

// Example 15
auto sort_much_less = [](auto first, auto last) {
    return std::sort(first, last, much_less());
};

当然,比较函数 much_less() 本身也是可调用的,也可以用 lambda 表达式实现:

// Example 15a
auto sort_much_less = [](auto first, auto last) {
    return std::sort(first, last,
        [](auto x, auto y) {
            static constexpr double tolerance = 0.2;
            return x < y && std::abs(x - y) > tolerance;
        }); 
};

容器适配器也很容易编写:

// Example 16
auto sort_much_less = [](auto& container) {
    return std::sort(container.begin(), container.end(), much_less());
};

需要注意的是,lambda 表达式不能以这种方式重载,它们实际上不是函数,而是对象。

3. std::bind 适配器

C++ 标准库提供了一个标准的可定制适配器 std::bind ,用于固定函数的某些参数。以下是使用示例:

// Example 17
using namespace std::placeholders; // For _1, _2 etc
int f3(int i, int j, int k) { return i + j + k; }
auto f2 = std::bind(f3, _1, _2, 42);
auto f1 = std::bind(f3, 5, _1, 7);
f2(2, 6);  // Returns 50
f1(3);     // Returns 15

std::bind 的第一个参数是要绑定的函数,其余参数按顺序排列。需要绑定的参数用指定的值替换,需要保持自由的参数用占位符 _1 , _2 等替换。返回值类型未指定,需要用 auto 捕获。返回值可以像函数一样调用,参数数量与占位符数量相同,也可以在任何需要可调用对象的上下文中使用。

然而, std::bind 也有局限性,它不能绑定模板函数。例如,以下代码无法编译:

auto sort_much_less = std::bind(std::sort, _1, _2, much_less()); // No!
4. 适配器与装饰器

适配器和装饰器的区别并不总是很清晰。装饰器模式用于增强现有接口,而适配器模式用于将接口转换为与期望不同接口的代码集成。例如,我们可以创建一个适配器,将系统调用 std::time 的结果转换为可打印的日期格式。 std::time 返回一个 std::time_t 类型的值,它是一个整数,表示自某个标准时刻(称为“纪元开始”)以来的秒数。另一个系统函数 localtime 将这个值转换为包含日期元素(年、月、日等)的结构体。

以下是实现美国日期格式适配器的示例:

// Example 18
class USA_Date {
    public:
        explicit USA_Date(std::time_t t) : t_(t) {}
        friend std::ostream& operator<<(std::ostream& out,
                                        const USA_Date& d) {
            const tm local_tm = *localtime(&d.t_);
            out << local_tm.tm_mon + 1 << "/" <<
                   local_tm.tm_mday << "/" <<
                   local_tm.tm_year + 1900;
            return out;
        }
    private:
        const std::time_t t_;
};

为了避免编写三个几乎相同的类来处理不同的日期格式(美国格式、欧洲格式和 ISO 格式),我们可以使用模板和“格式代码”来编码字段顺序:

// Example 19
template <size_t F> class Date {
    public:
        explicit Date(std::time_t t) : t_(t) {}
        friend std::ostream& operator<<(std::ostream& out,
                                        const Date& d) {
            const tm local_tm = *localtime(&d.t_);
            const int t[3] = { local_tm.tm_mday,
                               local_tm.tm_mon + 1,
                               local_tm.tm_year + 1900 };
            constexpr size_t i1 = F/100;
            constexpr size_t i2 = (F - i1*100)/10;
            constexpr size_t i3 = F - i1*100 - i2*10;
            static_assert(i1 >= 0 && i1 <= 2 && ..., "Bad format");
            out << t[i1] << "/" << t[i2] << "/" << t[i3];
            return out;
        }
    private:
        const std::time_t t_;
};
using USA_Date = Date<102>;
using European_Date = Date<12>;
using ISO_Date = Date<210>;

这个小包装器可以将整数类型适配为特定日期格式的代码使用,也可以看作是用 operator<<() 装饰整数。选择哪种描述方式取决于具体问题。

5. 编译时适配器

C++ 不仅有运行时接口,还有编译时接口,例如基于策略的设计。这些接口并不总是完全符合我们的需求,因此需要编写编译时适配器。

  • 策略式智能指针示例 :在基于策略的设计中,策略是类的构建块,允许程序员为特定行为定制实现。例如,我们可以实现一个基于策略的智能指针,它会自动删除所拥有的对象。策略是删除操作的具体实现:
// Chapter 15, Example 08
template <typename T,
          template <typename> class DeletionPolicy =
              DeleteByOperator>
class SmartPtr {
    public:
        explicit SmartPtr(T* p = nullptr,
                          const DeletionPolicy<T>& del_policy =
                              DeletionPolicy<T>())
            : p_(p), deletion_policy_(del_policy)
        {}
        ~SmartPtr() {
            deletion_policy_(p_);
        }
        // ... pointer interface ...
    private:
        T* p_;
        DeletionPolicy<T> deletion_policy_;
};

默认的删除策略是使用 operator delete

template <typename T> struct DeleteByOperator {
    void operator()(T* p) const {
        delete p;
    }
};

对于在用户指定堆上分配的对象,需要不同的删除策略,将内存返回给该堆:

template <typename T> struct DeleteHeap {
    explicit DeleteHeap(MyHeap& heap) : heap_(heap) {}
    void operator()(T* p) const {
        p->~T();
        heap_.deallocate(p);
    }
    private:
        MyHeap& heap_;
};

然后创建一个策略对象与指针一起使用:

MyHeap H;
SmartPtr<int, DeleteHeap<int>> p(new int, H);

这个策略不够灵活,只能处理一种类型的堆 MyHeap 。我们可以将堆类型作为第二个模板参数,使策略更通用:

// Example 20
template <typename T, typename Heap> struct DeleteHeap {
    explicit DeleteHeap(Heap& heap) : heap_(heap) {}
    void operator()(T* p) const {
        p->~T();
        heap_.deallocate(p);
    }
    private:
        Heap& heap_;
};

然而,这个策略与 SmartPtr 不兼容,因为 SmartPtr 模板期望第二个参数是一个单类型参数的模板,而 DeleteHeap 有两个类型参数。我们需要一个适配器将双参数模板转换为单参数模板,并固定第二个参数为特定的堆类型。可以使用继承创建适配器:

// Example 20
template <typename T>
struct DeleteMyHeap : public DeleteHeap<T, MyHeap> {
    using DeleteHeap<T, MyHeap>::DeleteHeap;
};

也可以使用模板别名:

// Example 21
template <typename T>
using DeleteMyHeap = DeleteHeap<T, MyHeap>;
6. 模板别名的局限性

模板别名在模板参数类型推导中不可见。例如,我们实现了一个流插入运算符,用于打印任何 STL 兼容序列容器的元素:

// Example 22
template <template <typename> class Container, typename T>
std::ostream& operator<<(std::ostream& out,
                         const Container<T>& c) {
    bool first = true;
    for (auto x : c) {
        if (!first) out << ", ";
        first = false;
        out << x;
    }
    return out;
}

这个模板函数有两个类型参数:容器类型和元素类型。容器本身是一个单类型参数的模板。编译器从第二个函数参数推导出容器类型和元素类型。

我们可以在一个简单的容器上测试这个插入运算符:

// Example 22
template <typename T> class Buffer {
    public:
        explicit Buffer(size_t N) : N_(N), buffer_(new T[N_]) {}
        ~Buffer() { delete [] buffer_; }
        T* begin() const { return buffer_; }
        T* end() const { return buffer_ + N_; }
        // ...
    private:
        const size_t N_;
        T* const buffer_;
};
Buffer<int> buffer(10);
// ... fill the buffer ...
cout << buffer; // Prints all elements of the buffer

但我们真正想要的是打印像 std::vector 这样的真实容器的元素:

std::vector<int> v;
// ... add some values to v ...
cout << v;

这段代码无法编译,因为 std::vector 实际上是一个双参数模板(第二个参数是分配器类型),而我们的流插入运算符声明为接受单参数的容器模板。我们可以使用别名创建适配器:

template <typename T> using vector1 = std::vector<T>;
vector1<int> v;
// ...
cout << v; // Does not compile either

这个代码仍然无法编译,因为模板别名在模板参数类型推导中不可见。在这种情况下,我们必须使用派生类适配器:

// Example 22
template <typename T>
struct vector1 : public std::vector<T> {
    using std::vector<T>::vector;
};
vector1<int> v;
// ...
cout << v;

总结

本文详细介绍了 C++ 中的适配器模式,包括函数适配器、 std::bind 适配器、适配器与装饰器的区别以及编译时适配器。通过多个示例展示了如何使用适配器模式解决不同的接口适配问题,同时也指出了模板别名在模板参数类型推导中的局限性。在实际编程中,根据具体需求选择合适的适配器方式可以提高代码的灵活性和可维护性。

C++ 适配器模式详解(续)

7. 适配器模式的应用场景总结

适配器模式在 C++ 编程中有广泛的应用场景,下面通过表格进行总结:
|应用场景|说明|示例|
| ---- | ---- | ---- |
|多线程环境接口适配|将对象接口适配为多线程安全的接口,保证多线程操作的正确性和一致性| locking_queue push() pop() 函数由同一互斥锁保护|
|函数参数绑定|固定函数的某些参数,简化函数调用|使用 std::bind 固定 f3 函数的部分参数|
|接口转换|将对象的当前接口转换为特定应用所需的接口|将 std::time 的结果转换为不同日期格式|
|模板参数适配|解决模板参数不匹配的问题|将双参数模板 DeleteHeap 转换为单参数模板 DeleteMyHeap |

下面是一个展示适配器模式应用流程的 mermaid 流程图:

graph TD
    A[原始对象或接口] --> B{是否需要适配}
    B -- 是 --> C[选择适配方式]
    C --> D[函数适配器]
    C --> E[std::bind 适配器]
    C --> F[编译时适配器]
    D --> G[修改函数参数或调用方式]
    E --> H[固定函数部分参数]
    F --> I[解决模板参数不匹配]
    B -- 否 --> J[直接使用]
    G --> K[适配后对象或接口]
    H --> K
    I --> K
    K --> L[应用于特定场景]
8. 不同适配器方式的比较

不同的适配器方式有各自的优缺点,下面通过表格进行比较:
|适配器方式|优点|缺点|适用场景|
| ---- | ---- | ---- | ---- |
|函数适配器(自定义)|灵活定制,可根据具体需求修改函数调用逻辑|代码编写相对复杂,需要手动实现适配逻辑|需要对函数调用进行复杂定制的场景|
| std::bind |标准库提供,使用方便,可固定函数部分参数|不能绑定模板函数|需要固定函数部分参数的简单场景|
|模板别名|代码简洁,可快速定义新的模板类型|在模板参数类型推导中不可见|不需要进行模板参数类型推导的场景|
|派生类适配器|可解决模板参数类型推导问题,能适配复杂的模板参数情况|代码量相对较大,需要继承和使用基类构造函数|需要进行模板参数类型推导,且模板参数不匹配的场景|

9. 适配器模式的注意事项
  • 命名冲突 :在使用适配器时,要注意避免命名冲突。例如,使用 lambda 表达式创建适配器时,不能有同名的 lambda 表达式,因为它们不能重载。
  • 模板参数匹配 :在编写编译时适配器时,要确保模板参数匹配。如 SmartPtr 模板期望的参数类型与 DeleteHeap 不匹配,需要进行适配。
  • 模板别名的局限性 :使用模板别名时,要清楚其在模板参数类型推导中的局限性,必要时使用派生类适配器。
10. 适配器模式的扩展应用

适配器模式可以与其他设计模式结合使用,进一步扩展其功能。例如,与装饰器模式结合,既可以实现接口的转换,又可以增强现有接口的功能。以下是一个简单的示例,展示如何将适配器模式与装饰器模式结合:

// 原始接口
class OriginalInterface {
public:
    virtual void operation() = 0;
};

// 具体实现类
class ConcreteImplementation : public OriginalInterface {
public:
    void operation() override {
        std::cout << "Original operation" << std::endl;
    }
};

// 适配器类
class Adapter : public OriginalInterface {
private:
    OriginalInterface* original;
public:
    Adapter(OriginalInterface* original) : original(original) {}
    void operation() override {
        // 适配操作
        std::cout << "Adapter operation: ";
        original->operation();
    }
};

// 装饰器类
class Decorator : public OriginalInterface {
private:
    OriginalInterface* original;
public:
    Decorator(OriginalInterface* original) : original(original) {}
    void operation() override {
        // 增强操作
        std::cout << "Decorator operation: ";
        original->operation();
    }
};

int main() {
    ConcreteImplementation concrete;
    Adapter adapter(&concrete);
    Decorator decorator(&adapter);
    decorator.operation();
    return 0;
}

在这个示例中, Adapter 类将 ConcreteImplementation 的接口进行了适配, Decorator 类对适配后的接口进行了增强。

总结

适配器模式是 C++ 中一种非常实用的设计模式,它可以帮助我们解决接口不兼容的问题,提高代码的灵活性和可维护性。通过本文的介绍,我们了解了适配器模式的多种应用方式,包括函数适配器、 std::bind 适配器、编译时适配器等,以及不同适配器方式的优缺点和适用场景。同时,我们还学习了适配器模式的注意事项和扩展应用。在实际编程中,我们应根据具体需求选择合适的适配器方式,并结合其他设计模式,以实现更高效、更灵活的代码。

内容概要:本文详细介绍了“秒杀商城”微服务架构的设计与实战全过程,涵盖系统从需求分析、服务拆分、技术选型到核心功能开发、分布式事务处理、容器化部署及监控链路追踪的完整流程。重点解决了高并发场景下的超卖问题,采用Redis预减库存、消息队列削峰、数据库乐观锁等手段保障数据一致性,并通过Nacos实现服务注册发现与配置管理,利用Seata处理跨服务分布式事务,结合RabbitMQ实现异步下单,提升系统吞吐能力。同时,项目支持Docker Compose快速部署和Kubernetes生产级编排,集成Sleuth+Zipkin链路追踪与Prometheus+Grafana监控体系,构建可观测性强的微服务系统。; 适合人群:具备Java基础和Spring Boot开发经验,熟悉微服务基本概念的中高级研发人员,尤其是希望深入理解高并发系统设计、分布式事务、服务治理等核心技术的开发者;适合工作2-5年、有志于转型微服务或提升架构能力的工程师; 使用场景及目标:①学习如何基于Spring Cloud Alibaba构建完整的微服务项目;②掌握秒杀场景下高并发、超卖控制、异步化、削峰填谷等关键技术方案;③实践分布式事务(Seata)、服务熔断降级、链路追踪、统一配置中心等企业级中间件的应用;④完成从本地开发到容器化部署的全流程落地; 阅读建议:建议按照文档提供的七个阶段循序渐进地动手实践,重点关注秒杀流程设计、服务间通信机制、分布式事务实现和系统性能优化部分,结合代码调试与监控工具深入理解各组件协作原理,真正掌握高并发微服务系统的构建能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值