一、概述
在本教程中,我们将使用 Hexagonal Architecture 的原理,使用 CLI 使用者实现一个简单的 Java CMS 应用程序。主要思想是尽可能保持业务逻辑分离,并使用 SOLID 原则中的“ D” 依赖反转原则来防止层之间的 耦合 。
2. 什么是六边形架构?
它是一种围绕业务逻辑设计软件应用程序架构并将其与其他层 解耦 的方法。解耦是通过使用端口和适配器来处理的,这就是为什么 Hexagonal Architecture 也被称为 Ports & Adapters 的原因。
您可以看到由 Hexagonal 架构分层的典型应用程序的几个关键特征;
- 您可以定义 端口 来说明您可以对特定域对象做什么,这些端口应该是 接口
- 有两种类型的端口; 入站(或驱动) 端口 和 出站(或驱动)端口
- 适配器 是端口的不同实现
- 有 2 种类型的适配器; 入站(或驱动) 适配器 和 出站(或驱动)适配器
- 领域对象从不依赖于外部层。
3. 组织项目结构
该项目包含 2 个根包:
- domain 用于 域对象 、 端口 和 用例 ,以说明域对象的契约和行为
- 基础设施 主要包含输入和输出端口的适配器实现
对于每个域对象,在我们的例子 中 ,我们有一个包来将端口和用例保存在域包中。在基础设施包中,我们为每个域对象提供了适配器包。
4. 领域模型
我们的领域模型,Article 是用来说明文章信息的,相关实现如下;
1 2 3 4 5 6 | public record Article( Long id, Long accountId, String title, String body ) {} |
这是一个简单的 Java POJO 类来声明文章信息,让我们看看如何在 Port 接口中编写合约
5. 端口
如果你想定义一个包含创建、检索和查询行为的端口,下面的接口将是一个不错的选择;
1 2 3 4 5 6 7 8 | public interface ArticlePort { Article create(ArticleCreate articleCreate); Article retrieve(Long articleId); List<Article> query(ArticleQuery articleQuery); } |
看上面的接口就可以理解返回值是一个领域模型,但是关于函数参数呢?
6.用例
应用程序由一个或多个用例组成,您可以在这些用例中使用端口作为依赖项,同时定义步骤以满足业务逻辑方面的要求。这里我们说端口,因为消费者不会直接使用它们,而是使用实现。
1 2 3 4 5 6 7 8 9 10 11 12 | 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 并且永远不知道实际的实现是什么,因为它不依赖于具体的实现。
用例请求也在用 例 包中,这是文章创建操作的示例
1 2 3 4 5 | public record ArticleCreate( Long accountId, String title, String body ) {} |
7. 适配器
在这部分之前,我们主要看到接口,在基础设施包中,我们将看到它们的实现。想想你依赖一个数据库,如果你想把数据持久化到数据库中,你需要使用 MySQL 、mongo 或 Cassandra 等 JPA 相关的技术……这些技术特定的实现都是适配器,它们需要满足我们在端口实现中提供的签名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 界面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 。通过使用这种表示法,您可以轻松地在测试中构建假实现,而不是尝试在持久层中模拟第三方库。执行此主应用程序后,您将看到以下内容
1 2 3 4 5 6 | 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. 结论
六边形架构帮助我们组织层以解耦域逻辑以拥有更多可维护的软件应用程序。这将我们的业务逻辑与外部层解耦,外部层通过在适配器中实现端口来访问域层。这将使您可以自由地将功能添加/更新/删除到您的业务逻辑中,而不必担心其他层上的问题。
您可以在此处访问示例的源代码: