kotlin - 在进行数据库读/写交互时,如何保持功能清洁架构中的用例和实体层纯净?
问题描述
介绍
在过去的几个月里,我一直在潜心研究函数式编程,因为我对 Kotlin 语言非常感兴趣,所以我一直在使用 Arrow 库来玩弄一些东西。
几周前,我一直在研究关于清洁架构的大学客座讲座,在此过程中,我偶然发现了 Mark Seemann 的这篇很棒的博客文章,描述了使用函数式编程如何自动导致清洁架构(或使用一种语言)像 Haskell 一样,编译器甚至可以强制执行 Clean Architecture)。
这激发了我想出一个餐厅预订软件的草稿(如果你有兴趣,检查和构建 repo 应该是轻而易举的)(忠于 Mark Seemann 的领域;))。但是,我不完全确定这个草案中的用例层是否可以称为纯粹的,我希望得到比我自己更多的 FP 经验和知识的人的反馈。
实体层
一个基本用例是尝试为我们餐厅的一定数量的座位创建新的预订。我已经通过以下方式为实体层建模:
fun reservationPossible(
requestedSeats: Int,
reservedSeats: Int,
capacity: Int
): Either<RequestedTooManySeats, ReservationPossible> =
if (reservedSeats + requestedSeats <= capacity) {
ReservationPossible(requestedSeats + reservedSeats).right()
} else {
RequestedTooManySeats.left()
}
const val CAPACITY = 10
object RequestedTooManySeats : Error()
sealed class Error
data class ReservationPossible(val newNumberOfReservedSeats: Int)
这里没有什么太花哨的东西,只是一个检查是否可以预订一定数量的请求座位的功能。一些错误和结果类也在下面以及(为了简单起见)const val
来模拟我们餐厅的容量。
框架/适配器 #1
为了在现实世界的应用程序中有意义,一些数据还需要存储在某种持久层中并从某种持久层中加载。所以,在我们的洋葱架构的最外层,会有一个我为这个例子模拟的数据库:
suspend fun getCurrentlyReservedSeats(): Either<ReadError, Int> {
delay(1) // ... get stuff from db
return 4.right()
}
suspend fun saveReservation(value: String, reservationPossible: ReservationPossible): Either<WriteError, Long> {
delay(1) // ... writing something to db
return 42L.right() // newRecordId
}
abstract class DbError : Error()
object ReadError : DbError()
object WriteError : DbError()
再说一次,这里没有太多事情……只是数据库读/写操作的存根。但是请注意,(按照 Arrow 提出的约定)这些函数用suspend
修饰符标记为不纯函数。
用例
现在来看用例,它基本上描述了我们的应用程序流程:
- 从 DB 获取当前保留的座位数
- 检查请求的座位数量是否仍然可用
- 如果是这样,保留新的保留
- 并返回新创建的预订 ID
它被翻译成reservationUseCase
函数中的代码:
data class UseCaseData(
val requestedSeats: Int,
val reservationName: String,
val getCurrentlyReservedSeats: suspend () -> Either<ReadError, Int>,
val writeVal: suspend (String, ReservationPossible) -> Either<WriteError, Long>,
)
fun reservationUseCase(data: UseCaseData): suspend () -> Either<Error, UseCaseResultData> = {
data.getCurrentlyReservedSeats()
.flatMap { reservationPossible(data.requestedSeats, it, CAPACITY) }
.flatMap { data.writeVal(data.reservationName, it) }
.flatMap { UseCaseResultData(it).right() }
}
data class UseCaseResultData(val newRecordId: Long)
这里是有趣的地方:这个函数接受一些UseCaseData
作为输入,并返回一个suspend
在程序入口处执行的函数,如下所示:
suspend fun main() {
reservationUseCase(
UseCaseData(
requestedSeats = 5,
reservationName = "John Dorian",
::getCurrentlyReservedSeats,
::saveReservation,
)
).invoke().fold(
ifLeft = { throw Exception(it.toString()) },
ifRight = { println(it.newRecordId) },
)
}
所以现在我的问题是:
reservationUseCase
函数本身可以被认为是纯的吗?我读过一些博客文章(不过,以 F# 作为示例语言)建议接收不纯函数作为参数的纯函数可能是纯函数,但不能保证是纯函数。reservationUseCase
在这个例子中,显然确实接收到了不纯的函数UseCaseData
。- 如果它不能被认为是纯粹的,那么如何编写一个像上面在 Kotlin 和 Arrow 中描述的那样的纯粹用例?
解决方案
正如您已经假设的那样,严格来说,reservationUseCase 不是一个纯函数。
我看到如何使它成为纯函数的唯一方法是直接传递所有需要的数据,而不是提供对该数据的访问的函数,但我怀疑这是否会使您的代码最终更干净或更易于阅读。
这将得出这样的结论:编排“工作流”的用例函数很少是纯粹的,因为几乎总是需要与某种存储库进行一些交互。
如果您希望某些核心逻辑是纯的,则必须将它们提取到仅接受和返回纯数据的函数中。
推荐阅读
- batch-file - 如何为放心项目创建 .bat 文件?
- html - 悬停时底部的三角形带边框
- sql - 如何存储工作日并检查日期是否与当天匹配
- android - Dagger 无法使用 Dagger Android 在 ViewModel 的构造函数上注入接口类型的参数
- google-cloud-platform - Google Stackdrive 自定义指标 - 数据保留期
- python-3.x - 我正在尝试将列表的正方形附加到列表中
- python - 使用 if elif 和 else 打印不同的语句
- java - 需要在 Alfresco 中通过 aspose 给 word 文档加水印
- shopware - 重定向到控制器抛出“不幸的是,出了点问题。”
- python-2.7 - 无法加载模块 pytesseract