C++中的六边形架构:如何不再害怕变更并爱上测试
您是否曾经重写过数据库模块,然后发现需要修改一半的业务逻辑?或者试图对某个类进行单元测试,却发现它拖拽了三吨来自外部世界的依赖?如果是这样——欢迎加入俱乐部。幸运的是,我们有一个工具可以一劳永逸地解决这些问题:六边形架构(也称为端口与适配器架构)。
在本文中,我们将探讨如何将这一强大概念应用于C++。您将看到的不是抽象的图表,而是可以立即使用的真实代码。是的,我们将以不狂热的方式进行——既不会让遗留项目崩溃,又能让新代码保持整洁。
什么是六边形架构(以及六边形与它有何关系)?
想象一下您应用程序的核心——这是纯粹的业务逻辑,它既不了解数据库,也不了解REST API,更不了解图形界面。在这个核心周围是端口(接口),再往外是适配器(具体实现)。从视觉上看,这类似于一个六边形,因此得名。
关键思想是:依赖关系指向内部。外部世界依赖于核心,而不是相反。这使得您可以在不更改业务逻辑的情况下更换数据库、通信协议或用户界面。听起来像梦想?让我们看看如何在C++中实现它。
经典问题:紧密耦合
典型的C++代码通常看起来像这样:
class OrderService {public: void createOrder(const OrderData& data) { // 业务逻辑 if (data.items.empty()) throw std::runtime_error("订单为空");
// 问题来了——直接依赖数据库 Database db("localhost", 5432); db.connect(); db.saveOrder(data); db.disconnect(); // 还依赖邮件服务 EmailSender sender("smtp.example.com"); sender.sendInvoice(data); }};这有什么问题?一切都有问题。您无法在没有真实数据库和邮件服务器的情况下测试createOrder。如果明天您决定从PostgreSQL切换到MongoDB——您将不得不重写OrderService。而如果邮件服务通过队列变为异步——业务逻辑又要再次修改。
使用六边形架构解决问题
步骤1:定义端口(接口)
端口是一个契约,它定义了核心需要从外部世界获得什么,但不定义如何实现。
// port/OrderRepository.hclass OrderRepository {public: virtual ~OrderRepository() = default; virtual void save(const Order& order) = 0; virtual std::optional<Order> findById(const std::string& id) = 0;};
// port/NotificationService.hclass NotificationService {public: virtual ~NotificationService() = default; virtual void sendInvoice(const Order& order) = 0;};请注意:这些是纯粹的抽象类。没有任何对框架、库或外部服务的依赖。只有业务实体。
步骤2:编写核心(业务逻辑)
现在,我们的服务只依赖于接口,而不是具体实现:
// core/OrderService.hclass OrderService {public: OrderService(OrderRepository& repo, NotificationService& notifier) : repository_(repo), notifier_(notifier) {}
void createOrder(const OrderData& data) { // 验证——纯粹的业务逻辑 if (data.items.empty()) { throw std::invalid_argument("订单不能为空"); }
Order order = Order::create(data); repository_.save(order); notifier_.sendInvoice(order); }
private: OrderRepository& repository_; NotificationServ