zoukankan      html  css  js  c++  java
  • 深入理解六边形架构

    六边形架构或六角架构是Alistair Cockburn在2005年提出,解决了传统的分层架构所带来的问题,实际上它也是一种分层架构,只不过不是上下或左右,而是变成了内部和外部。在领域驱动设计(DDD)和微服务架构中都出现了六边形架构的身影,在《实现领域驱动设计》一书中,作者将六边形架构应用到领域驱动设计的实现,六边形的内部代表了application和domain层,而在Chris Richardson对微服务架构模式的定义中,每个微服务使用六边形架构设计,足见六边形架构的重要性。那么让我们一探究竟,它为何如此受青睐。

    问题

    传统的分层架构具有广泛的应用,例如经典的三层架构,把系统分为表示层、业务逻辑层、数据访问层。在Martin Fowler的《企业应用架构模式》一书中做过深入阐述,本书04年出版,时至今日分层架构仍然是常用的设计方法,分层架构可以降低耦合、提高复用、分而治之,但同时也还是存在一些问题:

    • 应用逻辑在不同层泄露,导致替换某一层变得困难、难以对核心逻辑完整测试:你是否有过困惑,代码到底应该放在哪个层,虽然定义了各层的职责,但是总有人不严格遵循层次的分界,对于三层架构,常常会出现业务逻辑写在了表示层,或者业务逻辑直接和数据访问绑定。
    • 传统的分层架构是一维的结构,有时应用不光是上下的依赖,可能是多维的依赖,这时一维的结构就无法适应了。

    架构说明


    六边形架构又称为端口-适配器,这个名字更容器理解。六边形架构将系统分为内部(内部六边形)和外部,内部代表了应用的业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。内部通过端口和外部系统通信,端口代表了一定协议,以API呈现。一个端口可能对应多个外部系统,不同的外部系统需要使用不同的适配器,适配器负责对协议进行转换。这样就使得应用程序能够以一致的方式被用户、程序、自动化测试、批处理脚本所驱动,并且,可以在与实际运行的设备和数据库相隔离的情况下开发和测试。

    内涵

    六边形架构的重点体现在以下几个方面:

    • 关注点
      对于分层架构中层次的界定,Martin Fowler给出了一个判定的方法,就是如果把表示层换成其他实现,如果和原来的表示层有重复实现的内容,那么这部分内容就应该放到业务逻辑层。那么如何让开发人员在系统设计过程中始终保持这种视角,传统的分层架构是难以做到的。六边形架构有一个明确的关注点,从一开始就强调把重心放在业务逻辑上,外部的驱动逻辑或被驱动逻辑存在可变性、可替换性,依赖具体技术细节。而业务逻辑相对更加稳定,体现应用的核心价值,需要被详尽的测试。

    • 外部可替换
      一个端口对应多个适配器,是对一类外部系统的归纳,它体现了对外部的抽象。应用通过端口为外界提供服务,这些端口需要被良好的设计和测试。内部不关心外部如何使用端口,从一开始就要假定外部使用者是可替换的。六边形的六并没有实质意义,只是为了留足够的空间放置端口和适配器,一般端口数不会超过4个。适配器可以分为2类,“主”、“从”适配器,也可称为“驱动者”和“被驱动者”。

    • 自动测试
      在六边形架构中,自动化测试和用户具有同等的地位,在实现用户界面的同时就需要考虑自动化测试。它们对应相同的端口。六边形架构不仅让自动化测试这件事情成为设计第一要素,同时自动化测试也保证应用逻辑不会泄露到用户界面,在技术上保证了层次的分界。

    • 依赖倒置
      六边形架构必须遵循如下规则:内部相关的代码不能泄露到外部。所谓的泄露是指不能出现内部依赖外部的情况,只能外部依赖内部,这样才能保证外部是可以替换的。对于驱动者适配器,就是外部依赖内部的。但是对于被驱动者适配器,实际是内部依赖外部,这时需要使用依赖倒置,由驱动者适配器将被驱动者适配器注入到应用内部,这时端口的定义在应用内部,但是实现是由适配器实现。

    代码示例

    https://github.com/zhongpan/hexagonal-architecture-sample

    • app层

    app.h

    #pragma once
    
    namespace app {
        class RateRepository {
        public:
            virtual double getRate(double amount) = 0;
        };
        
        class Discounter {
        public:
            Discounter(RateRepository* rateRepository)
            : _rateRepository(rateRepository) {}
            
            double discount(double amount);
            
        private:
            RateRepository* _rateRepository;
        };
         
    }
    
    

    app.cpp

    #include "app.h"
    
    namespace app {
        
        double Discounter::discount(double amount) {
            if (amount <= 0) return 0;
            double rate = _rateRepository->getRate(amount);
            return amount * rate;
        }
    }
    
    • 命令行adapter

    cmd.cpp

    #include "app.h"
    #include "repo.h"
    #include <iostream>
    
    using namespace app;
    using namespace repo;
    
    int main(int argc, char* argv[]) {
        double amount = 0.0;
        std::cout << "please enter amount:";
        std::cin >> amount;
        MockRateRepository repo;
        Discounter app(&repo);
        std::cout << "discount is:" << app.discount(amount) << std::endl;
    }
    
    
    • 单元测试adapter

    test.cpp

    #define BOOST_TEST_MAIN
    #include "app.h"
    #include <boost/test/unit_test.hpp>
    #include <boost/smart_ptr.hpp>
    using namespace boost;
    
    class MockConstRateRepository : public app::RateRepository {
    public:
        MockConstRateRepository(double rate) : _rate(rate) {}
        double getRate(double amount) {
            return _rate;
        }
    private:
        double _rate;
    };
    
    BOOST_AUTO_TEST_SUITE(s_discount)
    
    BOOST_AUTO_TEST_CASE(t_discount)
    {
        double RATE_0point01 = 0.01;
        MockConstRateRepository repo(RATE_0point01);
        app::Discounter app(&repo);
        BOOST_CHECK_EQUAL(app.discount(100), 100*RATE_0point01);
        BOOST_CHECK_EQUAL(app.discount(0), 0);
        BOOST_CHECK_EQUAL(app.discount(-100), 0);
    }
    
    BOOST_AUTO_TEST_SUITE_END()
    
    • 持久化adapter

    repo.h

    #pragma once
    #include "app.h"
    
    namespace repo {
        class MockRateRepository : public app::RateRepository {
        public:
            double getRate(double amount);
        };
    }
    
    

    repo.cpp

    #include "repo.h"
    
    namespace repo {
        double MockRateRepository::getRate(double amount) {
            if (amount <= 100) return 0.01;
            if (amount <= 1000) return 0.02;
            return 0.05;
        }
    }
    
    
  • 相关阅读:
    Java核心API需要掌握的程度
    JAVA开发者最常去的20个英文网站
    Java .class files decompile tools
    让aptget到网络上查找源,避免经常插入光盘
    Google Job : Research Engineer Beijing
    教你亚健康该如何饮食
    tcpdump w 和 r 的使用
    黑客发布iOS 4.1永久越狱程序 狼人:
    微软将迎来迄今最大补丁日 一次修补49个漏洞 狼人:
    百度瞄准客户端 迅雷、360、腾讯皆是目标 狼人:
  • 原文地址:https://www.cnblogs.com/zhongpan/p/7606430.html
Copyright © 2011-2022 走看看