Closed jolestar closed 1 year ago
这个问题,以前我写过一点东西,供参考。
按照当前的 DDDML 规范,实体与实体之间只存在一种基本关系。这种关系从外层实体指向和它直接关联的内层实体。也就是说:从聚合根指向到它直接关联的聚合内部实体,或者从聚合内部的非聚合根实体指向它直接关联的与其在同一个聚合内的另一个非聚合根实体。
DDDML 认为实体之间只有这一种基本关系,实体之间的其他类型的关系都是在这种基本关系——或实体与值对象之间的其他基本关系——的基础上派生出来的关系。
举例说明:
aggregates:
Car:
id:
name: Id
type: string
properties:
Wheels:
itemType: Wheel
Tires:
itemType: Tire
entities:
# -----------------------------
Wheel:
id:
name: WheelId
type: WheelId
# ----------------------------------
# 如果没有特别声明,
# DDDML 工具可能也会为 Wheel 生成 Global ID 值对象,
# 值对象名称是 CarWheelId。
# ----------------------------------
# globalId:
# name: CarWheelId
# type: CarWheelId
# outerId:
# name: CarId
# -----------------------------
Tire:
id:
name: TireId
type: string
arbitrary: true
properties:
Positions:
itemType: Position
entities:
# ------------------------------------------------
# Position 是 Tire 直接关联的实体。
# 它描述“轮胎”什么时候在哪个“轮子”上,行驶了多少里程。
Position:
id:
name: PositionId
type: long
arbitrary: true
properties:
TimePeriod:
type: TimePeriod
MileAge:
type: long
WheelId:
type: WheelId
referenceType: Wheel
referenceName: Wheel
# -----------------------------
valueObjects:
TimePeriod:
properties:
From:
type: DateTime
To:
type: DateTime
# -----------------------------
enumObjects:
WheelId:
baseType: string
values:
LF:
description: left front
LR:
description: left rear
RF:
description: right front
RR:
description: right rear
在上面这个例子中,描述了不同实体之间的三个基本关系:
从聚合根 Car 到实体 Wheel 的关系。因为我们使用的是 OO 模型,所以这个关系需要体现为 Car 的一个属性,该属性名为 Wheels
,也可以说这个关系的名称叫 Wheels
。这个属性是一个 Set 语义的集合,集合的元素类型(itemType
)为 Wheel。换句话来说,属性 Wheels 的类型是“Wheel 的集合”。
从聚合根 Car 到实体 Tire 的关系。这个关系体现为 Car 的属性 Tires
,该属性的类型是 Tire 的集合。
从实体 Tire 到实体 Position 的关系。这个关系体现为实体 Tire 的属性 Positions
,该属性的类型是 Position 的集合。
也许读者看到这里会心存疑虑:在 DDDML 中,实体和实体之间只有这一种 One to Many 的基本关系够用么?像在 Hibernate ORM 框架中,实体的 One to Many 关系还支持几种不同语义的集合(Set、Bag、List、Map)呢。
实践证明是够用的。DDDML 鼓励优先使用值对象而不是引用对象(实体)。
其实,上面的例子中还描述了一个实体 Position 与实体 Wheel 之间的 Many to One 的关系,只是这个关系是一个派生关系。注意这个结点:/aggregates/Car/entities/Tire/entities/Position/properties/WheelId/referenceType
,它的值是 Wheel
。它的意思是:实体 Position 的属性 WheelId
的类型是值对象(枚举对象)WheelId
,通过这个属性的值,我们可以引用实体 Wheel 的一个实例——这个从实体 Positon 到实体 Wheel 的派生关系(“引用”)的名称是 Wheel(referenceName: Wheel
)。在后文我们会进一步讨论这里出现的“引用”。
Sui Move 目前的做法在我看来是够用的。
Sui 对象之间的关系我目前利用到的主要有两种:Wrapped,Owned by (parent) object。
其实我也并没有直接使用它们,我是通过它提供的 Table 来在代码层面实现上面我提到的 “DDDML 的实体之间的唯一一种基本关系”。而 Table 的内部实现中使用了 Sui 的这两种对象关系。Table 字段所在的那个对象 Wrap 了 Table 对象,Table 对象是 Dynamic Field 的 parent object。
另外,在有些时候,一个实体(聚合的状态)是不是可以修改、要“改成什么样”,是依赖于另外一个实体(聚合)的当前状态的。
也就是说,在有些时候,为了实现“聚合的方法”,我需要获取“对某个(聚合外的)对象的只读的引用”——但这个说不上是对象(实体)之间的关系。
比如,在这个 DDDML 模型中,Order 的 Create 方法需要获得 Product 对象的引用:
aggregates:
Order:
id:
name: Id
type: UID
arbitrary: true
properties:
TotalAmount:
type: u128
Items:
itemType: OrderItem
entities:
OrderItem:
id:
name: ProductId
type: String
properties:
Quantity:
type: u64
ItemAmount:
type: u128
methods:
Create:
isCreationCommand: true
parameters:
Product:
referenceType: Product
# eventPropertyName: Product
Quantity:
type: u64
event:
name: OrderCreated
properties:
UnitPrice:
type: u128
TotalAmount:
type: u128
Owner:
type: address
isOwner: true # Transfer the object to the account address indicated by this property
对于 Sui Move 来说,这个只读的对象引用是由客户端传过来的。在执行合约的时候,有必要检查对象引用中的版本和摘要,保证”用户是在看到对象的最新状态的情况下”发起的交易。
Design with @baichuan3 @wubuku
// Object Store design
module moveos_std::object{
struct Object<T>{
id: address,
value: T,
owner: address|shared|immutable,
}
borrow_value(object: &Object) : &T{
}
borrow_mut_value<T>(object: &mut Object<T>): &mut T{
//object is not immutable
}
public fun borrow<T>(signer, objectid): &Object<T>{
//check signer
//signer == owner or object is shared or immutable
}
public fun borrow_mut<T>(signer, objectid): &mut Object<T>{
//signer == owner or object is shared
}
public fun move_from<T>(signer, objectid): Object<T>{
//signer == owner or object is shared
}
// move_to == transfer
public fun move_to<T>(address, Object<T>){
//update Object owner.
}
remove<T>(Object<T>): T;
exists(objectid):bool;
}
使用现有的 Table(指的是 core Move 的 Table,不是 Sui Move 的 Table)来保存实体的时候,有个使用起来感觉很不方便的地方。
假设,我想在某个地方获取 Product 实体的信息,比如创建订单的时候,我想获取某个 Product 的“单价”(unit price),entry fun 只能传入 product Id,并不能(像 Sui Move 那样)传入 product 对象。那么,我总需要在某个地方通过 product Id 获取 product 对象或它的引用,然后再获取它的“单价”。
那么,想在 product 模块中提供类似下面的方法(其实是函数)是很自然的想法:
module aptos_demo::product {
public fun borrow_product(product_id: String): &Product acquires Tables {
let tables = borrow_global<Tables>(genesis_account::resouce_account_address());
table::borrow(&tables.product_table, product_id)
}
public fun unit_price(product: &Product): u128 {
product.unit_price
}
}
显然,上面的代码编译的时候会报错:
| table::borrow(&tables.product_table, product_id)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| Invalid return. Resource variable 'Tables' is still being borrowed.
| It is still being borrowed by this reference
有人可能觉得也许可以提供这样一对方法来“绕过”这个问题:
module aptos_demo::product {
public fun remove_product(product_id: String): Product acquires Tables {
let tables = borrow_global_mut<Tables>(genesis_account::resouce_account_address());
table::remove(&mut tables.product_table, product_id)
}
public fun add_product(product: Product) acquires Tables {
let tables = borrow_global_mut<Tables>(genesis_account::resouce_account_address());
table::add(&mut tables.product_table, product_id(&product), product);
}
}
也就是先使用 remove_product 来从 Table 中移除 product 对象,用完再把对象添加回 Table。不过这个做法可能存在问题。一个对象想要保存到 Table 里面,必须有 store ability。调用方拿到这个对象后,有可能把这个对象保存起来,而不只是“查看”一下它的属性值。
还有一个做法,就是提供这样的方法:
module aptos_demo::product {
public fun get_unit_price_by_product_id(product_id: String): u128 acquires Tables {
let tables = borrow_global<Tables>(genesis_account::resouce_account_address());
table::borrow(&tables.product_table, product_id).unit_price
}
}
对于简单的实体来说,好像是可以的。但是如果 product 是一个复杂的聚合的“聚合根”实体呢?
比如,在一个订单聚合内,聚合根 Order 下面存在 OrderItem 实体。我们怎么给其他模块提供获取 OrderItem 的 quantity 属性的方法呢?
可能可以这样写:
module aptos_demo::order {
struct Tables has key {
order_table: Table<String, Order>,
}
struct Order has store {
order_id: String,
version: u64,
total_amount: u128,
items: TableWithLength<String, OrderItem>,
}
public fun get_order_item_quantity(order_id: String, order_item_id: String): u64 acquires Tables {
let tables = borrow_global<Tables>(genesis_account::resouce_account_address());
let order = table::borrow(&tables.order_table, order_id);
let order_item = table_with_length::borrow(&order.items, order_item_id);
order_item::quantity(order_item) // 我们先假设 order_item 模块提供了这个 quantity 方法
}
}
不过感觉这有点太繁琐的(从这些方法的代码的实现 / 生成,以及调用方的使用体验来说,可能都有点)。
如果使用 core Move 的那个 table,上面需求的解决方案,貌似这个是相对来说比较简单的做法了:
As the implemention in #48, We do not need an ObjectRef and just use the ObjectID as the ref.
ObjectRef design and implemention
We implement the Object extension model on Move, and need a method to make a ref of an object.
This is part of #21
Why ObjectRef?
In software systems,
Entity
andRelationship
are often used to express design. Entities represent something with a unique identity and properties, which can be an actual object or a concept. Relationships are used to describe the connections between entities, which can take many forms, such as one-to-many, many-to-many, one-to-one, etc.We use
Object
to expressEntity
, so needObjectRef
to expressRelationship
.A blog system example
TBD