美文网首页
聊聊buckpal对于Hexagonal Architectur

聊聊buckpal对于Hexagonal Architectur

作者: go4it | 来源:发表于2021-03-20 22:17 被阅读0次

    本文主要赏析一下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对于Hexagonal Architectur

          本文链接:https://www.haomeiwen.com/subject/hhjgcltx.html