序
本文次要赏析一下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