Skip to main content

应用层编码

myddd将应用层拆分成两个子层。分别为

  • 应用层接口
  • 应用层实现

这样做的目的很明确,同样是为了将接口与实现能很好的隔离开来。这样做的好处在于:

  1. 对于调用应用层的UI/协议层来说,只需要关注应用层接口有什么功能,不用关心它是具体怎么实现的
  2. 应用层接口可以方便的发布成独立的JAR,这有利于RCP远程协议等其它方式。

1. 应用层接口

应用层接口中主要是由两个概念组成的:

  1. 应用接口
  2. DTO数据对象

1.1 DTO数据对象


data class ISVClientDTO
@ConstructorProperties(value = ["clientId","clientSecret","clientName","callback","description","extra"])
constructor(
var clientId:String? = null,
var clientSecret:String? = null,
var clientName:String,
var callback:String,
var description:String? = null,
var extra:ISVClientExtraDTO
):Shareable,Serializable

DTO通常在myddd-vertx中通常是由data class来实现,这样非常简洁易懂。

tip

DTO并不是实体的完全映射,你可以根据用例需要,来决定DTO包含哪些值,但大部分情况下,它可能与实体大多是一致的。

在myddd-vertx的DTO中,还有几个要素是特别注意的

用ConstructorProperties来辅助JSON序列及反序列化

ConstructorProperties是JDK的标准,在REST API,通常会存在JSON与对象的互相转换,由于应用接口层我们不希望依赖任何类似jacson的具体技术框架,因此我们建议使用ConstructorProperties来标注。

这样标注之后,就可以使用vert.x中的JSON序列与反序列方法,非常方便。而且又保持这一层对依赖的纯洁性。

DTO实现了Shareable与Serializable两个接口

这两个接口是为了缓存而使用的。后面在缓存的章节中我会专门讲到它,包括本地缓存及异步缓存都支持。

1.2 应用层接口

应用层接口很简单,根据你的系统用例来决定就好了。

interface ISVClientApplication {

suspend fun queryClientByClientId(clientId:String):Future<ISVClientDTO?>

suspend fun updateISVClient(isvClientDTO: ISVClientDTO):Future<ISVClientDTO>

suspend fun createISVClient(isvClientDTO: ISVClientDTO):Future<ISVClientDTO>

suspend fun listAllClients():Future<List<ISVClientDTO>>

}
caution

应用层接口,与调用它的,只能通过DTO对象与它们打交道,而不能把领域层的任何实体或对象做为依赖传递出去。

2. 单元测试

事实上,在DTO与接口定义好后,你就可以开始编写单元测试

class ISVClientApplicationTest : AbstractTest() {

private val isvClientApplication by lazy { InstanceFactory.getInstance(ISVClientApplication::class.java) }

@Test
fun testListAll(vertx: Vertx,testContext: VertxTestContext){
GlobalScope.launch(vertx.dispatcher()) {
try {
val isvClientDTO = randomISVClient()
val created = isvClientApplication.createISVClient(isvClientDTO).await()
testContext.verify {
Assertions.assertNotNull(created)
Assertions.assertNotNull(created.clientId)
Assertions.assertNotNull(created.clientSecret)
}


val queryAll = isvClientApplication.listAllClients().await()
testContext.verify { Assertions.assertTrue(queryAll.isNotEmpty()) }
}catch (t:Throwable){
testContext.failNow(t)
}
testContext.completeNow()
}
}

}

当然,现在它还无法测试通过,因为我们还只有接口而没有实现。

不用担心,这正在TDD所倡导的,我再说一次:

先编写一个无法通过的单元测试,再编码全它通过

3. 应用层实现

应用层实现就非常好理解了,它是应用层接口的实现。

应用层实现这一层在实现接口时,通常会依赖以下要素:

  1. 最主要的:领域层
  2. mdydd-vertx提供的查询通道
  3. 将其它需求申明为接口,同样由基础设施层实现

3.1 接口的实现


class ISVClientApplicationImpl : ISVClientApplication {

private val queryChannel by lazy { InstanceFactory.getInstance(QueryChannel::class.java) }

override suspend fun queryClientByClientId(clientId: String): Future<ISVClientDTO?> {
return try {
val isvClient = ISVClient.queryClient(clientId).await()
Future.succeededFuture(isvClient?.let { toISVClientDTO(it) })
}catch (t:Throwable){
Future.failedFuture(t)
}
}

override suspend fun updateISVClient(isvClientDTO: ISVClientDTO): Future<ISVClientDTO> {
return try {
val isvClient = toISVClient(isvClientDTO)
val updated = isvClient.updateISVClient().await()
Future.succeededFuture(toISVClientDTO(updated))
}catch (t:Throwable){
Future.failedFuture(t)
}
}

override suspend fun createISVClient(isvClientDTO: ISVClientDTO): Future<ISVClientDTO> {
return try {
val isvClient = toISVClient(isvClientDTO)
val created = isvClient.createISVClient().await()
Future.succeededFuture(toISVClientDTO(created))
}catch (t:Throwable){
Future.failedFuture(t)
}
}

override suspend fun listAllClients(): Future<List<ISVClientDTO>> {
return try {
val lists = queryChannel.queryList(QueryParam(
clazz = ISVClient::class.java,
sql = "from ISVClient"
)).await()
Future.succeededFuture(lists.stream().map { toISVClientDTO(it) }.toList())
}catch (t:Throwable){
Future.failedFuture(t)
}
}

}

从上面代码可以看出,应用层实现的逻辑大多数情况下是比较简单的,也就是,因为大多数情况下它是调用领域层的代码来实现的。

3.2 DTO组装器

应用实现层还有一个重要的行为,就是实体与DTO的互相转换,我们通常称它为DTO组装器

fun toISVClientDTO(isvClient:ISVClient) : ISVClientDTO {
return ISVClientDTO(
clientName = isvClient.clientName,
callback = isvClient.callback,
description = isvClient.description,
extra = toISVClientExtraDTO(isvClient.extra)!!,
clientId = isvClient.clientId,
clientSecret = isvClient.oauth2Client.clientSecret
)
}

fun toISVClient(isvClientDTO: ISVClientDTO) : ISVClient {
val extra = toISVClientExtra(isvClientDTO.extra)
checkNotNull(extra)
return ISVClient.createClient(clientId = isvClientDTO.clientId,clientName = isvClientDTO.clientName,callback = isvClientDTO.callback,extra = extra,description = isvClientDTO.description)
}

3.3 让单元测试通过

因为我们已经提供了实现,所以,是时候让我们的单元测试通过了。

记得在AbstractTest类中添加接口与实现的映射关系

@ExtendWith(VertxExtension::class)
abstract class AbstractTest {


companion object {

val randomIDString by lazy { InstanceFactory.getInstance(RandomIDString::class.java) }

init {
InstanceFactory.setInstanceProvider(GuiceInstanceProvider(Guice.createInjector(object : AbstractModule(){
override fun configure() {
bind(Vertx::class.java).toInstance(Vertx.vertx())
bind(WebClient::class.java).toInstance(WebClient.create(Vertx.vertx()))
bind(Mutiny.SessionFactory::class.java).toInstance(
Persistence.createEntityManagerFactory("default")
.unwrap(Mutiny.SessionFactory::class.java))

bind(RandomIDString::class.java).to(RandomIDStringProvider::class.java)
bind(IDGenerator::class.java).toInstance(SnowflakeDistributeId())

bind(ISVClientRepository::class.java).to(ISVClientRepositoryHibernate::class.java)
bind(ISVClientApplication::class.java).to(ISVClientApplicationImpl::class.java)

}
})))
}


}
}

然后,运行单元测试,它将会PASS。

如果没有,那恭喜你,你在很早期的时候就发现自己编码的错误了,而不是等完成一大堆功能后,才发现。

这将大大提升你的编码效率!!!