见:https://www.dddappp.org
从大的方面说,我们其实试图探讨一种可以极大地降低传统应用开发者迈入 Dapp 领域的门槛的开发模式。理想中的“极低门槛”应该是这样的:开发人员只需要构建领域模型,编写(存粹的)业务逻辑代码。这些代码理应可以在 L1/L2、链上/链下、中心化数据库/去中心化账本等不同的技术基础设施之间迁徙而不需要开发人员手动修改。
这显然是一个极具挑战性的目标。因为不同的区块链有不同的特性,我们的低代码平台的设计是否可以有效地应对基础设施的多样性所带来的挑战?对此我们当然是有极大的信心的,我们将通过开发一个 Demo 域名系统来证明这一点。
我们知道,之前 Move(Move 虚拟机)没有类似 Solidity(以太坊虚拟机)的 Mapping 这样的数据结构;就算接下来 Move 会支持 Table(Mapping),它也不应该被滥用。
因为对 Mapping 的滥用会造成所谓的区块链状态膨胀的问题。在 L1 链上的 Mapping 中存储大量的状态数据不是一个值得推荐的实践,它们应该存储在链下(或者说 L1 链外),但同时这些数据又需要在链上可以被验证和使用。
所以,在不使用 Table(Mapping)的前提下,在 Move 链上构建一个域名系统是一件具有相当复杂度的开发工作;接下来我们可以看到基于 DDD 和 DSL 的开发模式(低代码平台)可以如何极大地减轻开发者完成这项工作的负担。
第一步,我们手动编写这个 Demo 应用的实现代码(已完成);它们应该解耦为如下三部分:
第二步,我们制作代码生成工具,把上面的第 2 部分代码模板化(待完成)。然后,我们可以删除上面所说的第 2 部分代码,使用模型(加上代码模板)重新生成这部分代码,应用应该可以正常编译和运行。
需要说明的是,尽管我们还没有完成第二步,但是任何有经验的开发人员都可以通过阅读我们在第一步完成的源代码得到肯定的结论:真正的业务逻辑代码(它们确实需要开发人员编写)非常有限,源代码中的 90% 都是可以从模型生成的样板代码。
源代码见这里:
为便于在有限的时间内完成概念验证,我们生造一些简单但基本可以说明问题的“域名系统”的功能需求如下:
支持域名的注册与续费。提交域名注册的交易需要传入域名状态的不存在证明(Non-Membership Proof);提交续费域名的交易需要传入域名状态的存在证明(Membership Proof)。
只需要支持二级域名。这是为了演示实体 ID 不是一个“基本类型”,而是有两个字段的“复杂”值对象的情况。
Demo 域名系统由三部分组成:
需要说明的是,Demo 系统的实现代码实际上使用了 Starcoin 二层/分层方案中提到的富状态交易(StateFullTransaction) 模式。那么,如何确定交易(Transaction)涉及的状态(State)的边界,DDD 的“聚合”概念是一个非常强大的思维武器。
首先我们需要开发者使用 DSL(DDDML)描述 Demo 系统的领域模型。
这一步得到的领域模型可能如下:
aggregates:
DomainName:
id:
name: DomainNameId
type: DomainNameId
properties:
ExpirationDate:
type: u64
Owner:
type: AccountAddress
methods:
Register:
parameters:
Account:
type: signer
eventPropertyName: Owner
RegistrationPeriod:
type: u64
eventName: Registered
isCreationCommand: true
Renew:
parameters:
Account:
type: signer
RenewPeriod:
type: u64
eventName: Renewed
valueObjects:
DomainNameId:
properties:
TopLevelDomain: # TLD
type: string
SecondLevelDomain: # SLD
type: string
DomainNameId:一个描述域名 ID 的值对象。
DomainName:一个表示“域名”的聚合,聚合内只有一个同名的聚合根实体(DomainName),它有两个方法:
然后,开发者需要编写链上合约中的业务逻辑代码。
这个 Demo 系统的链上合约代码的目录和文件结构如下:
./move-contracts/src/modules
├── domain-name # 领域(域名系统)相关代码
│ ├── DomainName.move # 数据模型,DomainNameId、DomainNameState 等
│ ├── DomainNameAggregate.move # “域名”聚合的粘合代码
│ ├── DomainNameRegisterLogic.move # 注册域名的业务逻辑
│ ├── DomainNameRenewLogic.move # 域名续费的业务逻辑
│ └── DomainNameScripts.move # 脚本(script)函数入口
└── smt # Sparse Merkle Tree 相关代码,用于验证交易传入的状态证明
├── SMTHash.move
├── SMTProofUtils.move
├── SMTProofs.move # 对证明(Proof)进行验证的方法
├── SMTUtils.move
└── SMTreeHasher.move
这里需要特别说明的是,如果有代码生成工具,应该只有这两个文件是需要开发人员手动编写的“业务逻辑”代码:
除此之外的其他代码都是可以重用的库(smt 目录中的代码),或者是可以由(DSL描述的)模型生成的代码。
我们需要开发人员手写“注册域名”的业务逻辑(Move 代码)大致如下:
address 0x18351d311d32201149a4df2a9fc2db8a {
module DomainNameRegisterLogic {
//…
public fun verify(
account: &signer,
_domain_name_id: &DomainName::DomainNameId,
registration_period: u64,
): (
address, // Owner
u64, // RegistrationPeriod
) {
let amount = Account::withdraw<STC::STC>(account, 1000000);
Account::deposit(DomainName::genesis_account(), amount);
let e_owner = Signer::address_of(account);
let e_registration_period = registration_period;
(e_owner, e_registration_period)
}
public fun mutate(
domain_name_id: &DomainName::DomainNameId,
owner: address,
registration_period: u64,
): DomainName::DomainNameState {
let domain_name_state = DomainName::new_domain_name_state(
domain_name_id,
Timestamp::now_milliseconds() + registration_period,
owner,
);
domain_name_state
}
需要开发人员手写“域名续费”的业务逻辑大致如下:
address 0x18351d311d32201149a4df2a9fc2db8a {
module DomainNameRenewLogic {
//…
public fun verify(
account: &signer,
_domain_name_state: &DomainName::DomainNameState,
renew_period: u64,
): (
address, // Account
u64, // RenewPeriod
) {
let amount = Account::withdraw<STC::STC>(account, 1000000);
Account::deposit(DomainName::genesis_account(), amount);
let e_account = Signer::address_of(account);
let e_renew_period = renew_period;
(e_account, e_renew_period)
}
public fun mutate(
domain_name_state: &DomainName::DomainNameState,
_account: address,
renew_period: u64,
): DomainName::DomainNameState {
let updated_domain_name_state = DomainName::new_domain_name_state(
&DomainName::get_domain_name_state_domain_name_id(domain_name_state),
DomainName::get_domain_name_state_expiration_date(domain_name_state) + renew_period,
DomainName::get_domain_name_state_owner(domain_name_state),
);
updated_domain_name_state
}
以上,就是所有需要手写的代码:一个模型文件、两个业务逻辑代码文件!
Demo 系统的链下服务的代码是使用 Go 编写的。项目的目录和文件结构见下:
./off-chain-service
├── README.md
├── client # 域名系统的 Client SDK for Go
│ ├── client.go
│ └── client_test.go # 单元测试代码
├── contract # 链上合约的查询接口
│ ├── contract.go
│ └── contract_test.go # contract 包的单元测试代码
├── db
│ ├── bcs.go # 数据模型的 BCS 序列化/反序列化代码
│ ├── db.go # 数据库常量和接口代码
│ ├── db_test.go # db 包的单元测试代码
│ ├── models.go # 数据模型,DomainNameId、DomainNameEvent 等
│ ├── mysqldb.go # 数据访问层的 MySQL 实现
│ └── smt_test.go # 关于 SMT 的单元测试代码
├── events
│ ├── events_test.go # 单元测试代码
│ ├── lib.go # 从 serde-format/events.yaml 生成的事件结构和 BCS 序列化/反序列化代码
│ └── libext.go # 对 SerdeGen 工具生成的事件代码做的一些扩展
├── go.mod
├── go.sum
├── handlers.go # 使用 Gin 实现 RESTful API 的 handlers,依赖 starcoinmanager.go
├── main.go # 链下服务的程序入口
├── manager
│ ├── starcoinmanager.go # 拉取链上事件、更新链下状态,监控和处理链的分叉等
│ └── starcoinmanager_test.go # 单元测试代码
├── serde-format
│ └── events.yaml # 描述链上事件的格式的 YAML 文档,SerdeGen 工具可使用它们生成代码
├── tools # 一些工具类代码
│ ├── restclient.go # REST client 代码
│ ├── starcoinutil.go # 对 Starcoin Go SDK 做的一些包装和扩展
│ └── util.go # 其他工具类代码
├── transactions # (改变链上状态的)链上交易相关的代码
│ ├── lib.go # (改变链上状态的)链上交易的编码方法
│ ├── transactions_test.go # 单元测试代码
│ └── util.go # 一些关于链上交易的工具代码
└── vo
└── vo.go # RESTful API 使用的参数和返回值的类型(View Objects)
毫无疑问,我们完全可以制造自动化的工具,从 DDD 领域模型生成各种语言的 Client SDK,包括 Java Client SDK、JavaScript Client SDK、Go Client SDK,任意你能想到的编程语言的 Client SDK。
工具甚至能直接从领域模型生成有用户界面的前端应用,包括 Web 前端应用(这个在 Web 2.0 时代我们真的做过)、手机 App、命令行客户端应用等。也许你觉得这过于乐观,那么,最少你可以相信生成前端应用的脚手架代码是毫无问题的。
我们对这个 Demo 系统的代码行数做了一个粗略的统计如下:
github.com/AlDanial/cloc v 1.92 T=0.08 s (604.0 files/s, 97263.5 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Go 24 459 390 4374
Move 12 255 225 1323
JSON 3 0 0 509
Markdown 2 58 0 211
XML 5 0 0 118
YAML 2 5 0 66
TOML 2 24 0 34
-------------------------------------------------------------------------------
SUM: 50 801 615 6635
-------------------------------------------------------------------------------
通过这个 Demo,我们可以得出大致结论如下:
需要说明的是,Demo 其实没有完全展示出低代码开发的威力。我们可以借助 DSL(DDDML)的表现力,构建相当复杂的领域(对象)模型:值对象(非基本类型)嵌套值对象;包含多个(多级)实体的聚合等。在真实的“传统”应用的开发中,我们使用 DSL 构建过比 Demo 要复杂得多的聚合对象模型。