Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >聊聊buckpal对于Hexagonal Architecture的实践

聊聊buckpal对于Hexagonal Architecture的实践

原创
作者头像
code4it
修改于 2021-03-21 10:43:01
修改于 2021-03-21 10:43:01
76300
代码可运行
举报
文章被收录于专栏:码匠的流水账码匠的流水账
运行总次数:0
代码可运行

本文主要赏析一下buckpal对于Hexagonal Architecture的实践

项目结构

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
├── 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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【旧】G004Spring学习笔记-IOC案例
訾博ZiBo
2025/01/06
710
SpringCloud与Seata分布式事务初体验
在本篇文章中我们在SpringCloud环境下通过使用Seata来模拟用户购买商品时由于用户余额不足导致本次订单提交失败,来验证下在MySQL数据库内事务是否会回滚。
恒宇少年
2019/10/14
1.1K0
Spring知识梳理
Spring是模块化的,可以选择合适的模块来使用,其体系结构分为5个部分,分别为:
晚上没宵夜
2020/03/10
4780
聊聊go-bank-transfer项目对Clean Architecture的实践
本文主要赏析一下go-bank-transfer对于 Clean Architecture的实践
code4it
2021/03/21
4350
聊聊go-bank-transfer项目对Clean Architecture的实践
快速学习-使用 spring 的 IoC 的实现账户的CRUD
使用 spring 的 IoC 实现对象的管理 使用 DBAssit 作为持久层解决方案 使用 c3p0 数据源
cwl_java
2020/04/02
4100
Spring学习笔记(五)——JdbcTemplate和spring中声明式事务
它是 spring 框架中提供的一个对象,是对原始 Jdbc API 对象的简单封装。spring 框架为我们提供了很多的操作模板类。 1. 操作关系型数据的: JdbcTemplate HibernateTemplate 2. 操作 nosql 数据库的: RedisTemplate 3. 操作消息队列的: JmsTemplate spring中的JdbcTemplate在 spring-jdbc-5.0.2.RELEASE.jar 中,我们在导包的时候,除了要导入这个 jar 包外,还需要导入一个 spring-tx-5.0.2.RELEASE.jar(它是和事务相关的)。
不愿意做鱼的小鲸鱼
2022/09/24
1K0
Spring学习笔记(五)——JdbcTemplate和spring中声明式事务
Spring 基于注解配置方式的声明事务控制(注解方式)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
多凡
2019/11/01
8510
使用 spring 的 IoC 实现账户的 CRUD
大致步骤: 1.创建数据库 2.账户实体类 3.编写持久层代码 4.账户的持久层实现类 5.编写业务层代码 6.业务层实现类 7.配置文件 基本结构 1.创建数据库 create table account( id int primary key auto_increment, name varchar(40), money float )character set utf8 collate utf8_general_ci; insert into account(name,money) value
别团等shy哥发育
2023/02/27
2090
使用 spring 的 IoC 实现账户的 CRUD
利用Spring的IOC的实现数据库的CRUD
正文 引言: 纵然是基础,也还是有很多人不会; 先看一下完整的目录结构 打开你的图形化界面工具(sqlyog,navicat...),创建数据库spring-test,导入以下SQL DROP TA
框架师
2020/02/12
3760
利用Spring的IOC的实现数据库的CRUD
Dapr 客户端 搭配 WebApiClientCore 玩耍服务调用
public interface IBank { [HttpGet("/accounts/{accountId}")] Task<Account> GetUser(string accountId, CancellationToken cancellationToken = default);
张善友
2021/08/10
6870
Spring的IOC的实现账户的CRUD
Spring的IOC的实现账户的CRUD 完整目录结构 image.png 打开你的图形化界面工具(sqlyog,navicat...),创建数据库spring-test,导入以下SQL DROP TABLE IF EXISTS USER; CREATE TABLE USER ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT '用户名称', birthday DATETIME DEFAU
框架师
2021/03/05
2780
基于Spring的转账事务管理
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
多凡
2019/11/01
4130
【旧】G006Spring学习笔记-IOC案例完善
訾博ZiBo
2025/01/06
500
【旧】G006Spring学习笔记-IOC案例完善
快速学习-AOP 的相关概念
作用: 在程序运行期间,不修改源码对已有方法进行增强。 优势: 减少重复代码 提高开发效率 维护方便
cwl_java
2020/04/02
3430
MyBatis多表查询详解
MyBatis多表查询 0. 分析 一对一 一个用户只能拥有一个一个账户 sql语句: select u.*,a.id as aid,a.uid,a.money from account a,user u where a.uid =u.id; 一对多 一个用户可以拥有多个账户 sql语句 select u.*,a.id as aid ,a.uid,a.money from user u left outer join account a on u.id =a.uid 多对多
不愿意做鱼的小鲸鱼
2022/09/24
9740
MyBatis多表查询详解
Spring Bean.xml配置c3p0数据库连接池
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
多凡
2019/11/01
5820
Spring JdbcTemplate 使用及持久层继承JdbcDaoSupport XML配置
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
多凡
2019/11/01
4080
聊聊Ports and Adapters architecture
Ports and Adapters architecture,又叫Hexagonal architecture,其中ports层是六边形的边界,其中port又可以分为driver port及driven port,简单理解对应输入层及输出层;边界保护的是内部的app,其中app包括use cases或者叫做application services层以及domain层;adapter可以理解为将外部依赖进行适配,实现port层定义的接口
code4it
2021/03/15
4660
Mybatis使用注解实现一对多复杂关系映射
查询用户信息时,将用户的所有账户也查询出来,使用注解方式实现 (一个账户具有多个用户信息,所以形成了用户和账户之间的一对多关系) account表
别团等shy哥发育
2023/02/27
1.2K0
Mybatis使用注解实现一对多复杂关系映射
使用Spring编程式事务TransactionTemplate
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
多凡
2019/11/01
4.2K0
相关推荐
【旧】G004Spring学习笔记-IOC案例
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验