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的实现在哪一层?

当然是基础设施层了。