rooch-network / rooch

VApp Container with Move Language
https://rooch.network
Apache License 2.0
128 stars 54 forks source link

[ObjectRef] ObjectRef design and implemention #51

Closed jolestar closed 1 year ago

jolestar commented 1 year ago

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 and Relationship 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 express Entity, so need ObjectRef to express Relationship.

A blog system example

module moveos_std::object{
   struct ObjectRef<phantom T> has store {
      id: address,
   }

   public fun borrow<T>(ref: ObjectRef<T>): &Object<T>{
      //call native implemention
   }
}

module r_blog::tag{

  struct Tag {
    name: String,
  }

  struct TagRegistry{
     tags: table::Table<String, ObjectRef<Tag>>,
  }

  public fun get_or_create_tag(ctx: &mut TxContext, name: String): ObjectRef<Tag>{

     let registry = borrow_global_mut<TagRegistry>(@r_blog);
     //check repeat name
     if (table::contains(&registry.tags, name)) {
        let ref = table::borrow(&registry.tags, name);
        let object = object::borrow(ref);
        return object::as_object_ref(object);
     }else{
       let tag_obj = object::new_object(ctx, Tag{name});
       let ref = object::as_object_ref(&tag_obj);
       let ref2 = object::as_object_ref(&tag_obj);
       table::add(&mut registry.tags, name, ref);
       object::transfer(tag_object, tx_context::sender(ctx));
       return ref2 
     }    

     // Should we make the ObjectRef copyable?
     // why not just use the object id(address) as the ref?
  }

}

module r_blog::article{

  struct Article{
    title: String,
    author: address,
    tags: vector<ObjectRef<Tag>>,
  }

  public fun new_article(ctx: &mut TxContext, title: String, content: String, tags: vector<String>): Object<Article> {
     // get tags object ref by tags from `tag::get_or_create_tag`
     //create article object
  }

}

module r_blog::collection{

  struct ArticleCollection{
    articles: vector<ObjectRef<Article>>,
  }
  // How to make a collection? 
  // How can the owner's article collection be distinguished from the user's favorites collection?
}

TBD

  1. should we support borrowing mut object by ObjectRef?
wubuku commented 1 year ago

这个问题,以前我写过一点东西,供参考。

DDDML 的做法:实体之间只有一种基本关系

按照当前的 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

在上面这个例子中,描述了不同实体之间的三个基本关系:

也许读者看到这里会心存疑虑:在 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)。在后文我们会进一步讨论这里出现的“引用”。

wubuku commented 1 year ago

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 来说,这个只读的对象引用是由客户端传过来的。在执行合约的时候,有必要检查对象引用中的版本和摘要,保证”用户是在看到对象的最新状态的情况下”发起的交易。

jolestar commented 1 year ago

Design with @baichuan3 @wubuku

  1. Remove ObjectRef. Use raw object id(address or u256).
  2. Remove the Object arguments from the transaction.
  3. Provide Object Store API.
  4. Do not support embedded Object.
// 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;
}
wubuku commented 1 year ago

使用现有的 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 方法
    }

}

不过感觉这有点太繁琐的(从这些方法的代码的实现 / 生成,以及调用方的使用体验来说,可能都有点)。

wubuku commented 1 year ago

如果使用 core Move 的那个 table,上面需求的解决方案,貌似这个是相对来说比较简单的做法了:

jolestar commented 1 year ago

As the implemention in #48, We do not need an ObjectRef and just use the ObjectID as the ref.