大数跨境
0
0

Smart Domain实现DDD

Smart Domain实现DDD 摩尔线程
2024-01-31
0
导读:你好, 我是Alan, 前两篇文章我分别介绍了DDD的基础概念和事件风暴这一建模方法,今天这篇文章主要去介绍如

你好, 我是Alan, 前两篇文章我分别介绍了DDD的基础概念事件风暴这一建模方法,今天这篇文章主要去介绍如何使用smart domain去更好的实现DDD。 

同时这篇文章也是对徐昊老师的"如何使用smart domain实现DDD"这一视频中的内容整理。

DDD是一种模型驱动的设计方式, 通过对业务进行建模, 然后将模型与代码实现相关联, 而代码实现通常使用面向对象建模, 也就是将模型转化成为相应的对象实体。

那么对象实体间的关系是怎样的一种形式呢? 主要有两种 (1)引用关系 (2)聚合关系, 我画了一张图放在了下面。

但是我们需要注意的一点就是, DDD的全局实体之间的关系不仅表达了一种业务边界, 还隐藏了一种生命周期边界,例如实体1和实体2都位于内存中, 那么实体1消失, 实体2也会消失, 但是如果跨引用调用的, 很有可能出现两个实体之间位于不同的生命周期。

那么出现这种不一致的时候, 我们往往会使用repository或者将原本的聚合关系拆分成引用关系。

这两种做法实际上都不是很好,看似解决了这种生命周期边界的问题, 但是对于我们的模型是破坏性的, 最终导致我们的架构会退回到一种很复杂的模式上去。而原本我们希望通过DDD去设计出更好的架构的愿景,也变得遥遥无期了。

在解决生命周期问题之前, 我们先弄明白这个问题, 那就是我们希望从DDD中得到什么? 我认为是下面这样几点:

  1. 一组互联的模型和领域对象直接映射

  2. 具有恰当的抽象, 对于生命周期这样的实现细节应该被封装起来, 持久化的逻辑也是一种实现细节也应该被封装起来。


为了更好完成上面这两件事, smart domain这种实现方式就被设计出来。smart domain具有下面这几个特点

  • 直接使用面向对象

  • 所有的模型都会被建立成为一个完全连接的对象图

  • 所有的模型都会被映射成为RESTful API

  • 行为作为一种抽象层去隐藏实现细节

  • 概念模型, 模型和API上保持完整的一致


接下来就来说一下具体的smart domain实现步骤

首先实体(entity)不是一个单独的值(value), 还包含身份(identity)和关系(association)。

比如我们现在使用这种方式来表示我们的用户支付订单(order), 支付完成产生一个凭证(payment)。

使用事件建模方法对这个问题进行建模

然后根据使用smart domain方式建立一个完整的对象图:


通过对上面的对象图的遍历, 可以很快的得到对外的API接口。

  1. 用户关联了订单: user/orders-id

  2. 订单关联了支付凭证: orders-id/payment-id

  3. 用户关联了支付凭证: user/payment-id


一旦你通过关联, 作为一种对象模型去建立起来, 它就已经屏蔽了生命周期的差异。


我们也神奇的发现聚合也变成了一种实现细节。因为我们已经通过这种关联关系给业务边界建立起来。

我们的整体架构, 也变成了两层, 分别是domain层和api层

说完了上面的这些东西, 下面我们就通过一个简单的示例来实际进行演示一下, 这是一个简单的记账系统。系统会根据业务单据,按照不同的账目记录流水。比如,对于销售结算单,可能需要根据明细,分别向现金账户、信用账户、在途账户等账户中,记录流水。

我们建立的模型图是如下的:

然后使用我们的smart domain来改进上面的模型, 也就是添加association。于是我们的模型就变成下面这种:

通过上面的这种建模方法, 发现所有的关联关系都会变成一种接口。代码实现如下:

public class Customer implements Entity<StringCustomerDescription{
    private SourceEvidences sourceEvidences;

    private Accounts accounts;

    public HasMany<String, SourceEvidence<?>> sourceEvidences() {
        return sourceEvidences;
    }

    public HasMany<String, Account> accounts() {
        return accounts;
    }

    //customer.SoureceEvidences
    public interface SourceEvidences extends HasMany<StringSourceEvidence<?>> {
        SourceEvidence<?> add(SourceEvidenceDescription description);
    }

     //customer. Accounts
    public interface Accounts extends HasMany<StringAccount{
        void update(Account account, Account.AccountChange change);
    }
}

然后是source Evidence

public interface SourceEvidence<Description extends SourceEvidenceDescriptionextends Entity<StringDescription{

    interface Transactions extends HasMany<StringTransaction{
    }

    HasMany<String, Transaction> transactions();

    Map<String, List<TransactionDescription>> toTransactions();
}

通过遍历对象图, 我们可以得到对应的API

@Path("/customers")
public class CustomersApi {
    private Customers customers;

    @Inject
    public CustomersApi(Customers customers) {
        this.customers = customers;
    }

    @Path("{id}")
    public CustomerApi findById(@PathParam("id") String id) {
        return customers.findById(id).map(CustomerApi::new).orElse(null);
    }
}

最后根据需要提供关联对象的实现,并提供恰当的生命周期语义:Source Evidence和Transaction之间是内存直接关联。也就是说,每次读取Source Evidence时, 同时也会将关联的Transaction对象读入,也就是遵守聚合生命周期。而Account可能因为存在大量的Transaction,并不需要一起读入。也就是说,遵守引用生命周期。

reengineering.ddd.mybatis.memory提供的聚合语义

import reengineering.ddd.mybatis.memory.EntityList;

public class SourceEvidenceTransactions 
extends EntityList<StringTransactionimplements SourceEvidence.Transactions {
}

reengineering.ddd.mybatis.database提供的引用语义

import reengineering.ddd.mybatis.database.EntityList;

public class AccountTransactions 
extends EntityList<StringTransaction
implements Account.Transactions 
{

}

好了, 到这里这篇文章就到此结束了, 这篇文章介绍了如何使用smart domain来去让ddd变得更好。

如果觉得这篇文章对你有帮助的话, 还希望你给我点个赞。

【声明】内容源于网络
0
0
摩尔线程
摩尔线程以全功能 GPU 为核心,致力于向全球提供计算加速的基础设施和一站式解决方案,为各行各业的数智化转型提供强大的AI计算支持。
内容 301
粉丝 0
摩尔线程 摩尔线程以全功能 GPU 为核心,致力于向全球提供计算加速的基础设施和一站式解决方案,为各行各业的数智化转型提供强大的AI计算支持。
总阅读137
粉丝0
内容301