一、概述
在本教程中,我们将使用 Hexagonal Architecture 的原理,使用 CLI 使用者实现一个简单的 Java CMS 应用程序。主要思想是尽可能保持业务逻辑分离,并使用SOLID原则中的“ D”依赖反转原则来防止层之间的耦合。
2. 什么是六边形架构
它是一种围绕业务逻辑设计软件应用程序架构并将其与其他层解耦的方法。解耦是通过使用端口和适配器来处理的,这就是为什么 Hexagonal Architecture六边形架构 也被称为Ports & Adapters的原因。
您可以看到由 Hexagonal 架构分层的典型应用程序的几个关键特征;
3. 组织项目结构
该项目包含 2 个根包:
对于每个域对象,在我们的例子中,我们有一个包来将端口和用例保存在域包中。在基础设施包中,我们为每个域对象提供了适配器包。
4.领域模型
我们的领域模型,Article 是用来说明文章信息的,相关实现如下;
class="prettyprint hljs vbscript" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>publicb> record Article(
- Long id,
- Long accountId,
- String title,
- String body
- ) {}
这是一个简单的 Java POJO 类来声明文章信息,让我们看看如何在 Port 接口中编写合约
5. 端口
如果你想定义一个包含创建、检索和查询行为的端口,下面的接口将是一个不错的选择;
- <pre class="prettyprint hljs xml" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>publicb> <b>interfaceb> ArticlePort {
- Article create(ArticleCreate articleCreate);
- Article retrieve(Long articleId);
- List<Article> query(ArticleQuery articleQuery);
- }
看上面的接口就可以理解返回值是一个领域模型,但是关于函数参数呢?
6.用例
应用程序由一个或多个用例组成,您可以在这些用例中使用端口作为依赖项,同时定义步骤以满足业务逻辑方面的要求。这里我们说端口,因为消费者不会直接使用它们,而是使用实现。
- <pre class="prettyprint hljs xml" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>publicb> <b>classb> ArticleRetrieveUseCase {
- <b>finalb> ArticlePort articlePort;
- <b>publicb> ArticleRetrieveUseCase(ArticlePort articlePort) {
- <b>thisb>.articlePort = articlePort;
- }
- <b>publicb> Article retrieve(ArticleRetrieve useCase){
- <b>returnb> <b>thisb>.articlePort.retrieve(useCase.id());
- }
- }
在ArticleRetrieveUseCase类中,我们有一个检索用例函数,它使用ArticlePort并且永远不知道实际的实现是什么,因为它不依赖于具体的实现。
用例请求也在用例包中,这是文章创建操作的示例
class="prettyprint hljs vbscript" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>publicb> record ArticleCreate(
- Long accountId,
- String title,
- String body
- ) {}
(banq注:一个用例类似一个 [微服务 ]
7. 适配器
在这部分之前,我们主要看到接口,在基础设施包中,我们将看到它们的实现。
想想你依赖一个数据库,如果你想把数据持久化到数据库中,你需要使用 MySQL、mongo 或 Cassandra 等 JPA 相关的技术……这些技术特定的实现都是适配器,它们需要满足我们在端口实现中提供的签名。
class="prettyprint hljs xml" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>publicb> <b>classb> ArticleImMemoryDataAdapter implements ArticlePort {
- <b>privateb> <b>finalb> ConcurrentHashMap<Long,Article> articles = <b>newb> ConcurrentHashMap<>();
- @Override
- public Article create(ArticleCreate articleCreate) {
- <b>longb> id = (articles.size() + 1);
- Article article = <b>newb> Article(id, articleCreate.accountId(), articleCreate.title(),
- articleCreate.body());
- articles.put(id, article);
- <b>returnb> article;
- }
- @Override
- public Article retrieve(Long articleId) {
- <b>returnb> articles.get(articleId);
- }
- @Override
- public List<Article> query(ArticleQuery articleQuery) {
- <b>returnb> articles.values().stream()
- .filter(a-> a.accountId().equals(articleQuery.accountId()))
- .collect(Collectors.toList());
- }
- }
在上面的示例中,您可以看到持久层只是一个内存映射,用于保存文章数据并通过该映射获取。
8. 应用入口点
在大多数应用程序中,您会看到有 REST、gRPC 等接口让消费者通过该协议使用它。在这个例子中,我们将看到一个直接使用文章领域模型的公开用例的 CLI 界面。
class="prettyprint hljs xml" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>publicb> <b>classb> ArticleCli implements ArticlePort {
- <b>privateb> <b>finalb> ArticleCreateUseCase articleCreateUseCase;
- <b>privateb> <b>finalb> ArticleRetrieveUseCase articleRetrieveUseCase;
- <b>privateb> <b>finalb> ArticleQueryUseCase articleQueryUseCase;
- <b>publicb> ArticleCli(ArticleCreateUseCase articleCreateUseCase,
- ArticleRetrieveUseCase articleRetrieveUseCase,
- ArticleQueryUseCase articleQueryUseCase) {
- <b>thisb>.articleCreateUseCase = articleCreateUseCase;
- <b>thisb>.articleRetrieveUseCase = articleRetrieveUseCase;
- <b>thisb>.articleQueryUseCase = articleQueryUseCase;
- }
- @Override
- public Article create(ArticleCreate articleCreate) {
- ArticleCreate article = <b>newb> ArticleCreate(articleCreate.accountId(),articleCreate.title(),articleCreate.body());
- <b>returnb> <b>thisb>.articleCreateUseCase.create(article);
- }
- @Override
- public Article retrieve(Long articleId){
- <b>returnb> <b>thisb>.articleRetrieveUseCase.retrieve(ArticleRetrieve.from(articleId));
- }
- @Override
- public List<Article> query(ArticleQuery articleQuery) {
- <b>returnb> <b>thisb>.articleQueryUseCase.query(ArticleQuery.from(articleQuery.accountId()));
- }
- }
ArticleCli依赖于ArticlePort,让我们看看我们如何在示例 Java 应用程序中构建和使用ArticleCli 。
class="prettyprint hljs xml" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>publicb> <b>classb> Application {
- <b>staticb> Logger log = Logger.getLogger(Application.class.getName());
- <b>publicb> <b>staticb> <b>voidb> main(String args) {
- ArticleImMemoryDataAdapter articleImMemoryDataAdapter = <b>newb> ArticleImMemoryDataAdapter();
- ArticleCli articleCli = <b>newb> ArticleCli(
- <b>newb> ArticleCreateUseCase(articleImMemoryDataAdapter),
- <b>newb> ArticleRetrieveUseCase(articleImMemoryDataAdapter),
- <b>newb> ArticleQueryUseCase(articleImMemoryDataAdapter));
- Article article = articleCli.create(<b>newb> ArticleCreate(5L, <font>"Hexagonal in 5 Minutes"font><font>,
- font><font>"Hexagonal architecture is initially suggested..."font><font>));
- log.info(font><font>"Article is created "font><font> + article);
- Article articleDetails = articleCli.retrieve(1L);
- log.info(font><font>"Article details "font><font>+articleDetails);
- List<Article> result = articleCli.query(<b>newb> ArticleQuery(5L));
- log.info(font><font>"Found articles "font><font> + result);
- }
- }
- font>
ArticleCli接受ArticlePort的任何实现,在我们的例子中是ArticleImMemoryDataAdapter。通过使用这种表示法,您可以轻松地在测试中构建假实现,而不是尝试在持久层中模拟第三方库。
执行此主应用程序后,您将看到以下内容
class="prettyprint hljs css" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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 ]中防腐层,转换器、翻译器,用于不同上下文之间映射,也就不同微服务之间调用。正如端口有输入和输出一样,适配器也有输入和输出,输入时,适配器在端口调用之前,需要把其他上下文的数据适配到当前上下文,适配输入上下文或资源;输出时,适配器在端口之后,用于适配输出资源或上下文。)