本文次要赏析一下buckpal对于Hexagonal Architecture的实际

我的项目构造

├── adapter│   ├── in│   │   └── web│   │       └── SendMoneyController.java│   └── out│       └── persistence│           ├── AccountJpaEntity.java│           ├── AccountMapper.java│           ├── AccountPersistenceAdapter.java│           ├── ActivityJpaEntity.java│           ├── ActivityRepository.java│           └── SpringDataAccountRepository.java├── application│   ├── port│   │   ├── in│   │   │   ├── GetAccountBalanceQuery.java│   │   │   ├── SendMoneyCommand.java│   │   │   └── SendMoneyUseCase.java│   │   └── out│   │       ├── AccountLock.java│   │       ├── LoadAccountPort.java│   │       └── UpdateAccountStatePort.java│   └── service│       ├── GetAccountBalanceService.java│       ├── MoneyTransferProperties.java│       ├── NoOpAccountLock.java│       ├── SendMoneyService.java│       └── ThresholdExceededException.java└── domain    ├── Account.java    ├── Activity.java    ├── ActivityWindow.java    └── Money.java
这里分为adapter、application、domain三层;其中application层定义了port包,该包定义了in、out两种类型的接口;adapter层也分in、out两类,别离实现application/port层的接口;application的service则实现了port的接口

application/port

in

public interface GetAccountBalanceQuery {    Money getAccountBalance(AccountId accountId);}@Value@EqualsAndHashCode(callSuper = false)publicclass SendMoneyCommand extends SelfValidating<SendMoneyCommand> {    @NotNull    private final AccountId sourceAccountId;    @NotNull    private final AccountId targetAccountId;    @NotNull    private final Money money;    public SendMoneyCommand(            AccountId sourceAccountId,            AccountId targetAccountId,            Money money) {        this.sourceAccountId = sourceAccountId;        this.targetAccountId = targetAccountId;        this.money = money;        this.validateSelf();    }}public interface SendMoneyUseCase {    boolean sendMoney(SendMoneyCommand command);}
application/port/in定义了GetAccountBalanceQuery、SendMoneyUseCase接口

out

public interface AccountLock {    void lockAccount(Account.AccountId accountId);    void releaseAccount(Account.AccountId accountId);}public interface LoadAccountPort {    Account loadAccount(AccountId accountId, LocalDateTime baselineDate);}public interface UpdateAccountStatePort {    void updateActivities(Account account);}
application/port/out定义了AccountLock、LoadAccountPort、UpdateAccountStatePort接口

application/service

@RequiredArgsConstructorclass GetAccountBalanceService implements GetAccountBalanceQuery {    private final LoadAccountPort loadAccountPort;    @Override    public Money getAccountBalance(AccountId accountId) {        return loadAccountPort.loadAccount(accountId, LocalDateTime.now())                .calculateBalance();    }}@Componentclass NoOpAccountLock implements AccountLock {    @Override    public void lockAccount(AccountId accountId) {        // do nothing    }    @Override    public void releaseAccount(AccountId accountId) {        // do nothing    }}@RequiredArgsConstructor@UseCase@Transactionalpublic class SendMoneyService implements SendMoneyUseCase {    private final LoadAccountPort loadAccountPort;    private final AccountLock accountLock;    private final UpdateAccountStatePort updateAccountStatePort;    private final MoneyTransferProperties moneyTransferProperties;    @Override    public boolean sendMoney(SendMoneyCommand command) {        checkThreshold(command);        LocalDateTime baselineDate = LocalDateTime.now().minusDays(10);        Account sourceAccount = loadAccountPort.loadAccount(                command.getSourceAccountId(),                baselineDate);        Account targetAccount = loadAccountPort.loadAccount(                command.getTargetAccountId(),                baselineDate);        AccountId sourceAccountId = sourceAccount.getId()                .orElseThrow(() -> new IllegalStateException("expected source account ID not to be empty"));        AccountId targetAccountId = targetAccount.getId()                .orElseThrow(() -> new IllegalStateException("expected target account ID not to be empty"));        accountLock.lockAccount(sourceAccountId);        if (!sourceAccount.withdraw(command.getMoney(), targetAccountId)) {            accountLock.releaseAccount(sourceAccountId);            return false;        }        accountLock.lockAccount(targetAccountId);        if (!targetAccount.deposit(command.getMoney(), sourceAccountId)) {            accountLock.releaseAccount(sourceAccountId);            accountLock.releaseAccount(targetAccountId);            return false;        }        updateAccountStatePort.updateActivities(sourceAccount);        updateAccountStatePort.updateActivities(targetAccount);        accountLock.releaseAccount(sourceAccountId);        accountLock.releaseAccount(targetAccountId);        return true;    }    private void checkThreshold(SendMoneyCommand command) {        if(command.getMoney().isGreaterThan(moneyTransferProperties.getMaximumTransferThreshold())){            throw new ThresholdExceededException(moneyTransferProperties.getMaximumTransferThreshold(), command.getMoney());        }    }}
application/service的GetAccountBalanceService实现了application.port.in.GetAccountBalanceQuery接口;NoOpAccountLock实现了application.port.out.AccountLock接口;SendMoneyService实现了application.port.in.SendMoneyUseCase接口

domain

@AllArgsConstructor(access = AccessLevel.PRIVATE)public class Account {    /**     * The unique ID of the account.     */    @Getter private final AccountId id;    /**     * The baseline balance of the account. This was the balance of the account before the first     * activity in the activityWindow.     */    @Getter private final Money baselineBalance;    /**     * The window of latest activities on this account.     */    @Getter private final ActivityWindow activityWindow;    /**     * Creates an {@link Account} entity without an ID. Use to create a new entity that is not yet     * persisted.     */    public static Account withoutId(                    Money baselineBalance,                    ActivityWindow activityWindow) {        return new Account(null, baselineBalance, activityWindow);    }    /**     * Creates an {@link Account} entity with an ID. Use to reconstitute a persisted entity.     */    public static Account withId(                    AccountId accountId,                    Money baselineBalance,                    ActivityWindow activityWindow) {        return new Account(accountId, baselineBalance, activityWindow);    }    public Optional<AccountId> getId(){        return Optional.ofNullable(this.id);    }    /**     * Calculates the total balance of the account by adding the activity values to the baseline balance.     */    public Money calculateBalance() {        return Money.add(                this.baselineBalance,                this.activityWindow.calculateBalance(this.id));    }    /**     * Tries to withdraw a certain amount of money from this account.     * If successful, creates a new activity with a negative value.     * @return true if the withdrawal was successful, false if not.     */    public boolean withdraw(Money money, AccountId targetAccountId) {        if (!mayWithdraw(money)) {            return false;        }        Activity withdrawal = new Activity(                this.id,                this.id,                targetAccountId,                LocalDateTime.now(),                money);        this.activityWindow.addActivity(withdrawal);        return true;    }    private boolean mayWithdraw(Money money) {        return Money.add(                this.calculateBalance(),                money.negate())                .isPositiveOrZero();    }    /**     * Tries to deposit a certain amount of money to this account.     * If sucessful, creates a new activity with a positive value.     * @return true if the deposit was successful, false if not.     */    public boolean deposit(Money money, AccountId sourceAccountId) {        Activity deposit = new Activity(                this.id,                sourceAccountId,                this.id,                LocalDateTime.now(),                money);        this.activityWindow.addActivity(deposit);        return true;    }    @Value    public static class AccountId {        private Long value;    }}public class ActivityWindow {    /**     * The list of account activities within this window.     */    private List<Activity> activities;    /**     * The timestamp of the first activity within this window.     */    public LocalDateTime getStartTimestamp() {        return activities.stream()                .min(Comparator.comparing(Activity::getTimestamp))                .orElseThrow(IllegalStateException::new)                .getTimestamp();    }    /**     * The timestamp of the last activity within this window.     * @return     */    public LocalDateTime getEndTimestamp() {        return activities.stream()                .max(Comparator.comparing(Activity::getTimestamp))                .orElseThrow(IllegalStateException::new)                .getTimestamp();    }    /**     * Calculates the balance by summing up the values of all activities within this window.     */    public Money calculateBalance(AccountId accountId) {        Money depositBalance = activities.stream()                .filter(a -> a.getTargetAccountId().equals(accountId))                .map(Activity::getMoney)                .reduce(Money.ZERO, Money::add);        Money withdrawalBalance = activities.stream()                .filter(a -> a.getSourceAccountId().equals(accountId))                .map(Activity::getMoney)                .reduce(Money.ZERO, Money::add);        return Money.add(depositBalance, withdrawalBalance.negate());    }    public ActivityWindow(@NonNull List<Activity> activities) {        this.activities = activities;    }    public ActivityWindow(@NonNull Activity... activities) {        this.activities = new ArrayList<>(Arrays.asList(activities));    }    public List<Activity> getActivities() {        return Collections.unmodifiableList(this.activities);    }    public void addActivity(Activity activity) {        this.activities.add(activity);    }}
Account类定义了calculateBalance、withdraw、deposit办法;ActivityWindow类定义了calculateBalance办法

小结

buckpal工程adapter、application、domain三层;其中application层定义了port包,该包定义了in、out两种类型的接口;adapter层也分in、out两类,别离实现application/port层的接口;application的service则实现了port的接口。其中domain层不依赖任何层;application层的port定义了接口,而后service层实现接口和援用接口;adapter层则实现了application的port层的接口。

doc

  • buckpal