Skip to main content

领域层编码

领域层是业务的核心,因此,它也是整个系统中最具业务价值的一层。

接下来,我将向大家阐述如何在myddd-vertx的领域层进行编码。

1. 分析与建模#

建模是最开始应该做的一件事情。

建模就是对业务进行分析,然后将业务拆分为技术上的类与关系,聚合等。这在个过程中,你需要忘记数据库,无论背后是关系型还是非关系型。更不需要关心是基于什么具体的数据库。

建议使用draw.io这个免费开源的工具,来辅助你进行建模。

2. 领域类编码#

第一步:实现分析的建模

caution

由于绝大部分项目的存储仍然是使用关系型数据库,因此接下来我将还是以关系型数据库为技术实现背景来阐述。

完成建模后,就可以开始进行领域层的编码了。

myddd-vertx在关系型数据库方面是选用的hibernate-reactive来具体实现。但在领域层中,是基于JPA规范做为依赖,并未依赖任何hibernate的相关类或annotaion。

当然,JPA的绝大部分规范都是支持的,例如JPA中的@oneToMany或@ManytoMany等


@Entity@Table(name = "isv_client",    indexes = [        Index(name = "index_client_id",columnList = "client_id"),        Index(name = "index_primary_id",columnList = "primary_id")    ],    uniqueConstraints = [UniqueConstraint(columnNames = ["client_id"])])class ISVClient : BaseEntity() {
    @Column(name = "client_id",nullable = false,length = 36)    lateinit var clientId:String
    @Column(name = "client_type",nullable = false,length = 20)    lateinit var clientType:ISVClientType
    @Column(name = "primary_id",nullable = false,length = 64)    lateinit var primaryId:String
    @OneToOne(fetch = FetchType.EAGER,cascade = [CascadeType.ALL])    @JoinColumn(name = "relate_id", referencedColumnName = "id",nullable = false)    lateinit var oauth2Client:OAuth2Client
    @Column(name = "callback",nullable = false,length = 100)    lateinit var callback:String
    @Column(name = "extra",length = 500)    @Convert(converter = ISVClientExtraConverter::class)    lateinit var extra: ISVClientExtra
    @Column(name = "client_name",nullable = false,length = 200)    lateinit var clientName:String
    @Column(name = "description")    var description:String? = null
    @Column(name = "api_extra",length = 500)    @Convert(converter = ISVClientAuthExtraConverter::class)    var clientAuthExtra:ISVClientAuthExtra? = null
}

实体,大部分情况下你只需要继承BaseEntity,这是一个提供了乐观锁及基于雪花算法的ID生成策略的抽象父类。

如果你对于乐观锁或主键生成策略有不同的想法,可以选择实现org.myddd.vertx.domain.Entity接口,然后添加自有的相关逻辑。

上面的模型是对前面的建模的代码实现。

第二步: 定义仓储或其它抽象接口

如我在前面讲过,领域层不能依赖任何具体的技术框架,比如hibernate或redis等,但实际上我们的项目大多是关联存储的,主流也是关系型数据库。

myddd-vertx针对关系型数据库做了封装,以便你方便的使用它们。这些封装是基于hibernate-reactive而实现的。

当然,在领域层,你暂时还不需要关心这个。

接下来,我们将定义一个仓储接口


interface ISVClientRepository : EntityRepository {
    suspend fun querySuiteTicket(suiteId:String,clientType:ISVClientType):Future<ISVSuiteTicket?>
    suspend fun queryAuthCode(suiteId: String, domainId:String, orgCode:String, clientType: ISVClientType):Future<ISVAuthCode?>
    suspend fun queryTemporaryAuthCode(suiteId: String, domainId:String,orgCode:String, clientType: ISVClientType):Future<ISVAuthCode?>
    suspend fun queryPermanentAuthCode(suiteId: String, domainId:String, orgCode:String, clientType: ISVClientType):Future<ISVAuthCode?>}

如上所示,我们定义了一个接口,这个接口定义了一些数据操作行为。

note

EntityRepository也是个接口,它接供了基于Entity的常规数据操作

/** * 抽像仓储 */interface EntityRepository {
    /**     * 更新一个实体     */    suspend fun <T : Entity> save(entity: T): Future<T>
    /**     * 查询一个实体     */    suspend fun <T : Entity> get(clazz: Class<T>?, id: Serializable?): Future<T?>
    /**     * 实体是否存在     */    suspend fun <T : Entity> exists(clazz: Class<T>?, id: Serializable?): Future<Boolean>
    /**     * 批量更新     */    suspend fun <T : Entity> batchSave(entityList:Array<T>): Future<Boolean>
    /**     * 删除     */    suspend fun <T : Entity> delete(clazz: Class<T>?, id: Serializable?): Future<Boolean>
    /**     * 执行一个查询,返回一个LIST     */    suspend fun <T: Entity> listQuery(clazz: Class<T>?,sql:String,params:Map<String,Any> = HashMap()):Future<List<T>>

    /**     * 执行一个查询,返回单个数值     */    suspend fun <T:Entity> singleQuery(clazz: Class<T>?,sql:String,params:Map<String,Any> = HashMap()):Future<T?>
    /**     * 执行一个更新操作     */    suspend fun executeUpdate(sql:String,params:Map<String,Any> = HashMap()):Future<Int?>
}

这样,你定义了一个仓储的接口,这些接口行为是领域实体需要的。

这就足够了,你不需要在领域层关系这些接口是如何实现的,记住这一点,领域层依赖接口与抽象。


class ISVClient : BaseEntity() {
  companion object {        private val repository by lazy { InstanceFactory.getInstance(ISVClientRepository::class.java) }    }

    suspend fun createISVClient():Future<ISVClient>{        return repository.save(this)    }
}

如上代码所示,我们在实体中,以companion object的方式,提供了repository给实体使用。然后我们编写了一个创建ISVClient的领域方法,它的存储是通过repository来实现。

tip

InstanceFactory是实例工厂,通过它,你可以获取到任何接口或抽象的具体实现,这是依赖倒转

第三步:编写单元测试

myddd倡导使用单元测试来驱动我们的开发。所以现在是时候针对创建ISVClient来编写一个单元测试了。


class ISVClientTest : AbstractTest() {
    @Test    fun testCreateISVClient(vertx: Vertx,testContext: VertxTestContext){        GlobalScope.launch(vertx.dispatcher()) {            try {                val isvClient = ISVClient.createClient(clientName = UUID.randomUUID().toString(),extra = createISVExtra(),callback = "http://callback.workplus.io")
                val created = isvClient.createISVClient().await()                testContext.verify {                    Assertions.assertNotNull(created)                    Assertions.assertNotNull(created.oauth2Client)                    Assertions.assertTrue(created.getId() > 0)                }
            }catch (e:Exception){                testContext.failNow(e)            }            testContext.completeNow()        }    }
}

通常,我们在每一层的单元测试中,都会提供AbstractTest抽象类,它的一个最主要的作用是实例的初始化。

上面这个单元测试显然不能运行通过,因为我们定义了一个ISVClientRepository接口,但并未定义任何实现,对吧。

没有关系,TDD的原则就是

先编写一个无法通过的单元测试,然后编码,使之通过

当然,关于领域层的编码,我需要先暂停一下,接下来我们将提供ISVClientRepository的实现,然后使这个单元测试通过。

ISVClientRepository的实现在哪一层?

当然是基础设施层了。