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。

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

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