简化的 Java 六边形架构 – BABAL


一、概述
在本教程中,我们将使用 Hexagonal Architecture 的原理,使用 CLI 使用者实现一个简单的 Java CMS 应用程序。主要思想是尽可能保持业务逻辑分离,并使用SOLID原则中的“ D”依赖反转原则来防止层之间的耦合。

2. 什么是六边形架构?
它是一种围绕业务逻辑设计软件应用程序架构并将其与其他层解耦的方法。解耦是通过使用端口和适配器来处理的,这就是为什么 Hexagonal Architecture六边形架构 也被称为Ports & Adapters的原因。

您可以看到由 Hexagonal 架构分层的典型应用程序的几个关键特征;

  • 您可以定义端口来说明您可以对特定域对象做什么,这些端口应该是接口
  • 有两种类型的端口;入站(或驱动) 端口和出站(或驱动)端口
  • 适配器是端口的不同实现
  • 有 2 种类型的适配器;入站(或驱动) 适配器和出站(或驱动)适配器
  • 领域对象从不依赖于外部层。

3. 组织项目结构

该项目包含 2 个根包:

  • domain 用于域对象、端口和用例,以说明域对象的契约和行为
  • 基础设施主要包含输入和输出端口的适配器实现

对于每个域对象,在我们的例子中,我们有一个包来将端口和用例保存在域包中。在基础设施包中,我们为每个域对象提供了适配器包。

4.领域模型
我们的领域模型,Article 是用来说明文章信息的,相关实现如下;

public record Article(
    Long id,
    Long accountId,
    String title,
    String body
) {}

这是一个简单的 Java POJO 类来声明文章信息,让我们看看如何在 Port 接口中编写合约

5. 端口
如果你想定义一个包含创建、检索和查询行为的端口,下面的接口将是一个不错的选择;

public interface ArticlePort {
    Article create(ArticleCreate articleCreate);
    Article retrieve(Long articleId);
    List<Article> query(ArticleQuery articleQuery);
}

看上面的接口就可以理解返回值是一个领域模型,但是关于函数参数呢?

6.用例
应用程序由一个或多个用例组成,您可以在这些用例中使用端口作为依赖项,同时定义步骤以满足业务逻辑方面的要求。这里我们说端口,因为消费者不会直接使用它们,而是使用实现。

public class ArticleRetrieveUseCase {
    final ArticlePort articlePort;
    public ArticleRetrieveUseCase(ArticlePort articlePort) {
        this.articlePort = articlePort;
    }
    public Article retrieve(ArticleRetrieve useCase){
        return this.articlePort.retrieve(useCase.id());
    }
}

在ArticleRetrieveUseCase类中,我们有一个检索用例函数,它使用ArticlePort并且永远不知道实际的实现是什么,因为它不依赖于具体的实现。
用例请求也在用例包中,这是文章创建操作的示例

public record ArticleCreate(
    Long accountId,
    String title,
    String body
) {}

(banq注:一个用例类似一个微服务)

7. 适配器
在这部分之前,我们主要看到接口,在基础设施包中,我们将看到它们的实现。
想想你依赖一个数据库,如果你想把数据持久化到数据库中,你需要使用 MySQL、mongo 或 Cassandra 等 JPA 相关的技术……这些技术特定的实现都是适配器,它们需要满足我们在端口实现中提供的签名。

public class ArticleImMemoryDataAdapter implements ArticlePort {
    private final ConcurrentHashMap<Long,Article> articles = new ConcurrentHashMap<>();
    @Override
    public Article create(ArticleCreate articleCreate) {
        long id = (articles.size() + 1);
        Article article = new Article(id, articleCreate.accountId(), articleCreate.title(),
            articleCreate.body());
        articles.put(id, article);
        return article;
    }
    @Override
    public Article retrieve(Long articleId) {
        return articles.get(articleId);
    }
    @Override
    public List<Article> query(ArticleQuery articleQuery) {
        return articles.values().stream()
            .filter(a-> a.accountId().equals(articleQuery.accountId()))
            .collect(Collectors.toList());
    }
}

在上面的示例中,您可以看到持久层只是一个内存映射,用于保存文章数据并通过该映射获取。

8. 应用入口点
在大多数应用程序中,您会看到有 REST、gRPC 等接口让消费者通过该协议使用它。在这个例子中,我们将看到一个直接使用文章领域模型的公开用例的 CLI 界面。

public class ArticleCli implements ArticlePort {
    private  final ArticleCreateUseCase articleCreateUseCase;
    private final ArticleRetrieveUseCase articleRetrieveUseCase;
    private final ArticleQueryUseCase articleQueryUseCase;
    public ArticleCli(ArticleCreateUseCase articleCreateUseCase,
        ArticleRetrieveUseCase articleRetrieveUseCase,
        ArticleQueryUseCase articleQueryUseCase) {
        this.articleCreateUseCase = articleCreateUseCase;
        this.articleRetrieveUseCase = articleRetrieveUseCase;
        this.articleQueryUseCase = articleQueryUseCase;
    }
    @Override
    public Article create(ArticleCreate articleCreate) {
        ArticleCreate article = new ArticleCreate(articleCreate.accountId(),articleCreate.title(),articleCreate.body());
        return this.articleCreateUseCase.create(article);
    }
    @Override
    public Article retrieve(Long articleId){
        return this.articleRetrieveUseCase.retrieve(ArticleRetrieve.from(articleId));
    }
    @Override
    public List<Article> query(ArticleQuery articleQuery) {
        return this.articleQueryUseCase.query(ArticleQuery.from(articleQuery.accountId()));
    }
}

ArticleCli依赖于ArticlePort,让我们看看我们如何在示例 Java 应用程序中构建和使用ArticleCli 。
public class Application {
    static Logger log = Logger.getLogger(Application.class.getName());
    public static void main(String args) {
        ArticleImMemoryDataAdapter articleImMemoryDataAdapter = new ArticleImMemoryDataAdapter();
        ArticleCli articleCli = new ArticleCli(
            new ArticleCreateUseCase(articleImMemoryDataAdapter),
            new ArticleRetrieveUseCase(articleImMemoryDataAdapter),
            new ArticleQueryUseCase(articleImMemoryDataAdapter));
        Article article = articleCli.create(new ArticleCreate(5L, "Hexagonal in 5 Minutes",
            "Hexagonal architecture is initially suggested..."));
        log.info("Article is created " + article);
        Article articleDetails = articleCli.retrieve(1L);
        log.info("Article details "+articleDetails);
        List<Article> result = articleCli.query(new ArticleQuery(5L));
        log.info("Found articles " + result);
    }
}

ArticleCli接受ArticlePort的任何实现,在我们的例子中是ArticleImMemoryDataAdapter。通过使用这种表示法,您可以轻松地在测试中构建假实现,而不是尝试在持久层中模拟第三方库。
执行此主应用程序后,您将看到以下内容
Oct 23, 2021 12:26:49 AM com.huseyin.hexagonal4j.Application main
INFO: Article is created Article[id=1, accountId=5, title=Hexagonal in 5 Minutes, body=Hexagonal architecture is initially suggested...]
Oct 23, 2021 12:26:49 AM com.huseyin.hexagonal4j.Application main
INFO: Article details Article[id=1, accountId=5, title=Hexagonal in 5 Minutes, body=Hexagonal architecture is initially suggested...]
Oct 23, 2021 12:26:49 AM com.huseyin.hexagonal4j.Application main
INFO: Found articles [Article[id=1, accountId=5, title=Hexagonal in 5 Minutes, body=Hexagonal architecture is initially suggested...]]

9. 结论
六边形架构帮助我们组织层以解耦域逻辑以拥有更多可维护的软件应用程序。这将我们的业务逻辑与外部层解耦,外部层通过在适配器中实现端口来访问域层。这将使您可以自由地将功能添加/更新/删除到您的业务逻辑中,而不必担心其他层上的问题。

您可以在此处访问示例的源代码

(banq注“适配器类似DDD中防腐层,转换器、翻译器,用于不同上下文之间映射,也就不同微服务之间调用。正如端口有输入和输出一样,适配器也有输入和输出,输入时,适配器在端口调用之前,需要把其他上下文的数据适配到当前上下文,适配输入上下文或资源;输出时,适配器在端口之后,用于适配输出资源或上下文。)