首页  >  文章  >  Java  >  使用有界上下文构建 Java 应用程序

使用有界上下文构建 Java 应用程序

Linda Hamilton
Linda Hamilton原创
2024-11-07 18:40:03456浏览

Using bounded contexts to build a Java application

什么是有界上下文?

有界上下文是领域驱动设计(DDD)的核心模式之一。它代表了如何将一个大项目划分为多个域。这种分离提供了灵活性并且更容易维护。

什么是六边形架构?

六边形架构将应用程序的核心与其外部依赖项分开。它使用端口和适配器将业务逻辑与外部服务分离。使业务逻辑独立于框架、数据库或用户界面,使应用程序能够轻松适应未来的需求。

该架构由三个主要组件组成:

  1. 业务模型:业务规则和核心逻辑。与外部依赖完全隔离,仅通过端口进行通信。
  2. 端口:商业模式的退出和进入。它们将核心与外层分开。
  3. 适配器:将外部交互(HTTP 请求、数据库操作)转换为核心可以理解的内容。有用于传入通信的输入适配器和用于传出通信的输出适配器。

为什么要使用六边形架构?

  • 可测试性:您可以为业务逻辑编写单元测试,而无需模拟数据库、外部 API 或框架。
  • 可维护性:您可以轻松地交换依赖项,而不影响核心业务逻辑。
  • 可扩展性:各层独立缩放,提高整体性能。
  • 灵活性:不同的外部系统可以与相同的核心逻辑交互。

使用 Java 中的六角形架构构建应用程序

这个演练项目在 Java 中使用有界上下文和六边形架构。

我们的目标是为一个名为 Techtopia 的游乐园创建一个票务系统。该项目有 3 个主要的有界上下文:门票、景点和入口。每个有界上下文都有其目录,并包括诸如 in 和我们的端口、适配器、用例等组件。

我们将逐步完成购买公园门票的代码流程。

  1. 定义域

创建一个“domain”目录并包含业务逻辑,不受任何框架或外部依赖。

创建“Ticket”实体。

package java.boundedContextA.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;

@Getter
@Setter
@ToString
public class Ticket {
    private TicketUUID ticketUUID;
    private LocalDateTime start;
    private LocalDateTime end;
    private double price;
    private TicketAction ticketAction;
    private final Guest.GuestUUID owner;
    private ActivityWindow activityWindow;

    public record TicketUUID(UUID uuid) {
    }

    public Ticket(TicketUUID ticketUUID, Guest.GuestUUID owner) {
        this.ticketUUID = ticketUUID;
        this.owner = owner;
    }

    public Ticket(TicketUUID ticketUUID, LocalDateTime start, LocalDateTime end, double price, TicketAction ticketAction, Guest.GuestUUID owner) {
        this.ticketUUID = ticketUUID;
        this.start = start;
        this.end = end;
        this.price = price;
        this.ticketAction = ticketAction;
        this.owner = owner;
    }

    public Ticket(TicketUUID ticketUUID, LocalDateTime start, LocalDateTime end, double price, Guest.GuestUUID owner, ActivityWindow activityWindow) {
        this.ticketUUID = ticketUUID;
        this.start = start;
        this.end = end;
        this.price = price;
        this.owner = owner;
        this.activityWindow = activityWindow;
    }

    public void addTicketActivity(TicketActivity ticketActivity) {
        this.activityWindow.add(ticketActivity);
    }
}

此外,创建另一个名为“BuyTicket”的域类。

package java.boundedContextA.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;

@Getter
@Setter
@ToString
public class Ticket {
    private TicketUUID ticketUUID;
    private LocalDateTime start;
    private LocalDateTime end;
    private double price;
    private TicketAction ticketAction;
    private final Guest.GuestUUID owner;
    private ActivityWindow activityWindow;

    public record TicketUUID(UUID uuid) {
    }

    public Ticket(TicketUUID ticketUUID, Guest.GuestUUID owner) {
        this.ticketUUID = ticketUUID;
        this.owner = owner;
    }

    public Ticket(TicketUUID ticketUUID, LocalDateTime start, LocalDateTime end, double price, TicketAction ticketAction, Guest.GuestUUID owner) {
        this.ticketUUID = ticketUUID;
        this.start = start;
        this.end = end;
        this.price = price;
        this.ticketAction = ticketAction;
        this.owner = owner;
    }

    public Ticket(TicketUUID ticketUUID, LocalDateTime start, LocalDateTime end, double price, Guest.GuestUUID owner, ActivityWindow activityWindow) {
        this.ticketUUID = ticketUUID;
        this.start = start;
        this.end = end;
        this.price = price;
        this.owner = owner;
        this.activityWindow = activityWindow;
    }

    public void addTicketActivity(TicketActivity ticketActivity) {
        this.activityWindow.add(ticketActivity);
    }
}

*BuyTicket *代表买票逻辑。通过使其成为一个单独的 Spring 组件,您可以将购票逻辑隔离在其类中,该逻辑可以独立于其他组件而发展。这种分离提高了可维护性并使代码库更加模块化。

  1. 创建端口

在“ports/in”目录中创建用例。在这里,我们将制作购买门票的用例。

package java.boundedContextA.domain;

import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.UUID;

@Component
public class BuyTicket {
    public Ticket buyTicket(TicketAction ticketAction, LocalDateTime start, LocalDateTime end, double price, Guest.GuestUUID owner) {
        return new Ticket(new Ticket.TicketUUID(UUID.randomUUID()), start, end, price, ticketAction, owner);
    }
}

创建工单记录并保存。

package java.boundedContextA.ports.in;

public interface BuyingATicketUseCase {
    void buyTicket(BuyTicketsAmountCommand buyTicketsAmountCommand);
}

接下来,在“ports/out”目录中创建代表购买所述门票的每个步骤的端口。创建诸如 "CreateTicketPort""TicketLoadPort""TicketUpdatePort".
等接口

package java.boundedContextA.ports.in;

import java.boundedContextA.domain.Guest;
import java.boundedContextA.domain.TicketAction;

import java.time.LocalDateTime;

public record BuyTicketsAmountCommand(double price, TicketAction action, LocalDateTime start, LocalDateTime end, Guest.GuestUUID owner) {}
  1. 创建端口接口

在一个名为“core”的单独目录中,实现购票用例的接口。

package java.boundedContextA.ports.out;

import java.boundedContextA.domain.Ticket;

public interface TicketCreatePort {
    void createTicket(Ticket ticket);
}
  1. 创建适配器

“adapters/out”目录中,创建Ticket的JPA实体来镜像域。这就是应用程序与数据库通信并创建门票表的方式。

package java.boundedContextA.core;

import java.boundedContextA.domain.BuyTicket;
import java.boundedContextA.ports.in.BuyTicketsAmountCommand;
import java.boundedContextA.ports.in.BuyingATicketUseCase;
import java.boundedContextA.ports.out.TicketCreatePort;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@AllArgsConstructor
public class DefaultBuyingATicketUseCase implements BuyingATicketUseCase {
    final BuyTicket buyTicket;
    private final List<TicketCreatePort> ticketCreatePorts;

    @Override
    public void buyTicket(BuyTicketsAmountCommand buyTicketsAmountCommand) {
        var ticket = buyTicket.buyTicket(buyTicketsAmountCommand.action(), buyTicketsAmountCommand.start(), buyTicketsAmountCommand.end(), buyTicketsAmountCommand.price(), buyTicketsAmountCommand.owner());
        ticketCreatePorts.stream().forEach(ticketCreatedPort -> ticketCreatedPort.createTicket(ticket));
    }
}

不要忘记创建实体的存储库。这个存储库将与服务进行通信,就像任何其他架构一样。

package java.adapters.out.db;

import java.boundedContextA.domain.TicketAction;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;

import java.sql.Types;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(schema="boundedContextA",name = "boundedContextA.tickets")
@Getter
@Setter
@NoArgsConstructor
public class TicketBoughtJpaEntity {
    @Id
    @JdbcTypeCode(Types.VARCHAR)
    private UUID uuid;

    public TicketBoughtJpaEntity(UUID uuid) {
        this.uuid = uuid;
    }

    @JdbcTypeCode(Types.VARCHAR)
    private UUID owner;

    @Column
    private LocalDateTime start;
    @Column
    private LocalDateTime end;

    @Column
    private double price;
}

“adapters/in”目录中,创建Ticket的控制器。此应用程序将与外部源通信。

package java.boundedContextA.adapters.out.db;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.UUID;

public interface TicketRepository extends JpaRepository<TicketBoughtJpaEntity, UUID> {
    Optional<TicketBoughtJpaEntity> findByOwner(UUID uuid);
}
  1. 完成购票流程

要表示门票已购买,请在 “events” 目录中创建活动记录。

事件表示应用程序中发生的重大事件,对于系统与其他系统或组件的通信非常重要。它们是与外界沟通已完成的流程、已更改的状态或需要采取进一步行动的另一种方式。

package java.boundedContextA.adapters.in;

import java.boundedContextA.ports.in.BuyTicketsAmountCommand;
import java.boundedContextA.ports.in.BuyingATicketUseCase;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class TicketsController {
    private final BuyingATicketUseCase buyingATicketUseCase;

    public TicketsController(BuyingATicketUseCase buyingATicketUseCase) {
        this.buyingATicketUseCase = buyingATicketUseCase;
    }

    @PostMapping("/ticket")
    public void receiveMoney(@RequestBody BuyTicketsAmountCommand command) {
        try {
            buyingATicketUseCase.buyTicket(command);
        } catch (IllegalArgumentException e) {
            System.out.println("An IllegalArgumentException occurred: " + e.getMessage());
        }
    }
}

不要忘记包含一个主类来同时运行所有内容。

package java.boundedContextA.events;

import java.time.LocalDateTime;
import java.util.UUID;

public record TicketIsBoughtEvent(UUID uuid, LocalDateTime start, LocalDateTime end) {
}

**这是一个非常简短的解释,有关更深入的代码以及如何连接到 React 接口,请查看此 GitHub 存储库:https://github.com/alexiacismaru/techtopia。

结论

在 Java 中实现此架构涉及使用业务逻辑和接口定义一个干净的核心域,创建适配器以与外部系统交互,以及将所有内容编写在一起,同时保持核心隔离。

通过遵循此架构,您的 Java 应用程序将具有更好的结构、更易于维护并且足够灵活以适应未来的变化。

以上是使用有界上下文构建 Java 应用程序的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn