MCPcopy Create free account
hub / github.com/Re-engineering-Domain-Driven-Design/Accounting

github.com/Re-engineering-Domain-Driven-Design/Accounting @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
289 symbols 1,140 edges 68 files 0 documented · 0% updated 4y ago★ 1803 open issues

Browse by type

Functions 212 Types & classes 77
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

业务背景

这是一个简单的记账系统。系统会根据业务单据,按照不同的账目记录流水。比如,对于销售结算单,可能需要根据明细, 分别向现金账户、信用账户、在途账户等账户中,记录流水。

业务模型如下: 模型

Smart Domain架构模式

关于Smart Domain架构模式这里有个简单的介绍。

通过关联对象构建模型

应用Smart Domain架构模式,作为领域驱动设计的实现模式。首先将模型图中的关联关系建模为关联对象。 注意,需要额外引入一个根关联对象。在这个模型中是Customers:

关联

关联对象用接口表示。出于我个人的口味,我使用了内部接口。具体的代码在domain模块中:

public class Customer implements Entity<String, CustomerDescription> {
    private SourceEvidences sourceEvidences;

    private Accounts accounts;

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

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

    public interface SourceEvidences extends HasMany<String, SourceEvidence<?>> {
        SourceEvidence<?> add(SourceEvidenceDescription description);
    }

    public interface Accounts extends HasMany<String, Account> {
        void update(Account account, Account.AccountChange change);
    }
}

public class Account implements Entity<String, AccountDescription> {
    private Transactions transactions;

    public HasMany<String, Transaction> transactions() {
        return transactions;
    }

    public interface Transactions extends HasMany<String, Transaction> {
        Transaction add(Account account, SourceEvidence<?> evidence, TransactionDescription description);
    }
}

public interface SourceEvidence<Description extends SourceEvidenceDescription> extends Entity<String, Description> {
    HasMany<String, Transaction> transactions();

    interface Transactions extends HasMany<String, Transaction> {
    }
}

注意,我是用了宽接口(可修改)而对外仅仅暴露窄接口(只读)。这样可以让我将修改逻辑封装到实体对象之内。

将模型映射为API

在构建了模型之后,可以将模型映射为API,通过关联关系,表示URI:

API

然后使用JAX-RS将根关联对象转成root resource:

@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);
    }
}

实体转成sub-resources:

public class CustomerApi {
    private Customer customer;

    public CustomerApi(Customer customer) {
        this.customer = customer;
    }

    @GET
    public CustomerModel get(@Context UriInfo info) {
        return new CustomerModel(customer, info);
    }

    @Path("source-evidences")
    public SourceEvidencesApi sourceEvidences(@Context ResourceContext context) {
        return context.initResource(new SourceEvidencesApi(customer));
    }

    @Path("accounts")
    public AccountsApi accounts() {
        return new AccountsApi(customer);
    }
}

关联对象可以转成sub-resource,如下代码所示,实际是对于Customer.SourceEvidences的API化:

public class SourceEvidencesApi {
    private Customer customer;

    @Inject
    private SourceEvidenceReader reader;

    public SourceEvidencesApi(Customer customer) {
        this.customer = customer;
    }

    @GET
    @Path("{evidence-id}")
    public SourceEvidenceModel findById(@PathParam("evidence-id") String id,
                                        @Context UriInfo info) {
        return customer.sourceEvidences().findByIdentity(id).map(evidence -> SourceEvidenceModel.of(customer, evidence, info))
                .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
    }

    @GET
    public CollectionModel<SourceEvidenceModel> findAll(@Context UriInfo info, @DefaultValue("0") @QueryParam("page") int page) {
        return new Pagination<>(customer.sourceEvidences().findAll(), 40).page(page,
                evidence -> SourceEvidenceModel.simple(customer, evidence, info),
                p -> sourceEvidences(info).queryParam("page", p).build(customer.getIdentity()));
    }

    @POST
    public Response create(String json, @Context UriInfo info) {
        SourceEvidence evidence = customer.add(reader.read(json)
                .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_ACCEPTABLE)).description());
        return Response.created(ApiTemplates.sourceEvidence(info).build(customer.getIdentity(), evidence.getIdentity())).build();
    }
}

API的具体代码都在api模块中。可以看到,在测试中,并没有使用数据库,而是依赖关联对象的抽象,完成api的测试。

实现关联对象

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

生命周期

Transaction的不同生命周期,可能极度违反直觉。因为从业务概念上讲,Account应该聚合Transaction才对。这种业务上的聚合关系,我们通过URI来表示。 也就是对于Transaction而言,它的URI是/customers/{cid}/accounts/{aid}/transactions/{tid}。 而不是/customers/{cid}/source-evidences/{sid}/transactions/{tid}。 将业务聚合与生命周期混为一谈,是领域驱动设计的顽疾。

聚合的生命周期由reengineering.ddd.mybatis.memory包提供:

import reengineering.ddd.mybatis.memory.EntityList;

public class SourceEvidenceTransactions extends EntityList<String, Transaction> implements SourceEvidence.Transactions {
}

引用的生命周期由reengineering.ddd.mybatis.database提供:

import reengineering.ddd.mybatis.database.EntityList;

public class AccountTransactions extends EntityList<String, Transaction> implements Account.Transactions {

}

所有的关联对象都使用MyBatis实现,代码在persistent/mybatis模块中。

Extension points exported contracts — how you extend this code

browse all types & interfaces →

Core symbols most depended-on inside this repo

browse all functions →

Shape

Method 212
Class 60
Interface 16
Enum 1

Languages

Java100%

Modules by API surface

persistent/mybatis/src/test/java/reengineering/ddd/accounting/ModelMapperTest.java18 symbols
persistent/mybatis/src/test/java/reengineering/ddd/accounting/AssociationsTest.java16 symbols
persistent/mybatis/src/main/java/reengineering/ddd/accounting/mybatis/ModelMapper.java15 symbols
persistent/mybatis/src/main/java/reengineering/ddd/mybatis/database/EntityList.java13 symbols
domain/src/main/java/reengineering/ddd/accounting/model/Customer.java11 symbols
domain/src/main/java/reengineering/ddd/accounting/model/Account.java9 symbols
domain/src/main/java/reengineering/ddd/accounting/description/SalesSettlementDescription.java9 symbols
api/src/test/java/reengineering/ddd/accounting/api/CustomerSourceEvidencesApiTest.java9 symbols
domain/src/test/java/reengineering/ddd/accounting/model/CustomerTest.java8 symbols
persistent/mybatis/src/test/java/reengineering/ddd/accounting/TestDataMapper.java7 symbols
persistent/mybatis/src/main/java/reengineering/ddd/mybatis/memory/EntityList.java7 symbols
domain/src/main/java/reengineering/ddd/accounting/model/Transaction.java6 symbols

For agents

$ claude mcp add Accounting \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page