共计 8146 个字符,预计需要花费 21 分钟才能阅读完成。
序
本文次要赏析一下 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)
public
class 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
@RequiredArgsConstructor
class GetAccountBalanceService implements GetAccountBalanceQuery {
private final LoadAccountPort loadAccountPort;
@Override
public Money getAccountBalance(AccountId accountId) {return loadAccountPort.loadAccount(accountId, LocalDateTime.now())
.calculateBalance();}
}
@Component
class NoOpAccountLock implements AccountLock {
@Override
public void lockAccount(AccountId accountId) {// do nothing}
@Override
public void releaseAccount(AccountId accountId) {// do nothing}
}
@RequiredArgsConstructor
@UseCase
@Transactional
public 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