我尝试简化在Spray中为HTTP请求提供响应的验证过程(我使用Slick进行数据库访问)。目前,我检查一个查询是否应该进一步查询以下查询(返回错误)。最终会出现嵌套模式匹配。每个验证案例都可以返回不同的错误,因此我无法使用任何flatMap。
class LocationDao {
val db = DbProvider.db
// Database tables
val devices = Devices.devices
val locations = Locations.locations
val programs = Programs.programs
val accessTokens = AccessTokens.accessTokens
def loginDevice(deviceSerialNumber: String, login: String, password: String): Either[Error, LocationResponse] = {
try {
db withSession { implicit session =>
val deviceRowOption = devices.filter(d => d.serialNumber === deviceSerialNumber).map(d => (d.id, d.currentLocationId.?, d.serialNumber.?)).firstOption
deviceRowOption match {
case Some(deviceRow) => {
val locationRowOption = locations.filter(l => l.id === deviceRow._2.getOrElse(0L) && l.login === login && l.password === password).firstOption
locationRowOption match {
case Some(locationRow) => {
val programRowOption = programs.filter(p => p.id === locationRow.programId).firstOption
programRowOption match {
case Some(programRow) => {
val program = Program(programRow.name, programRow.logo, programRow.moneyLevel, programRow.pointsForLevel,
programRow.description, programRow.rules, programRow.dailyCustomerScansLimit)
val locationData = LocationData(program)
val locationResponse = LocationResponse("access_token", System.currentTimeMillis(), locationData)
Right(locationResponse)
}
case None => Left(ProgramNotExistError)
}
}
case None => Left(IncorrectLoginOrPasswordError)
}
}
case None => Left(DeviceNotExistError)
}
}
} catch {
case ex: SQLException =>
Left(DatabaseError)
}
}
}
简化此操作的好方法是什么?也许还有其他方法......
答案 0 :(得分:8)
通常,您可以使用for-comprehension将此处的许多monadic结构(包括Try
,Option
和Either
)链接在一起而不进行嵌套。例如:
for {
val1 <- Try("123".toInt)
} yield for {
val2 <- Some(val1).map(_ * 2)
val3 = Some(val2 - 55)
val4 <- val3
} yield val4 * 2
在你的风格中,这可能看起来像:
Try("123".toInt) match {
case Success(val1) => {
val val2 = Some(val1).map(_ * 2)
val2 match {
case Some(val2value) => {
val val3 = Some(val2value - 55)
val3 match {
case Some(val4) => Some(val4)
case None => None
}
}
case None => None
}
case f:Failure => None
}
}
答案 1 :(得分:1)
您可以为控制流量Eiter
定义帮助方法。
这样做的好处是,您将拥有很好的控制力和灵活性。
def eitherMe[ I, T ]( eitherIn: Either[ Error, Option[ I ] ],
err: () => Error,
block: ( I ) => Either[ Error, Option[ T ] ]
): Either[ Error, Option[ T ] ] = {
eitherIn match {
case Right( oi ) => oi match {
case Some( i ) => block( i )
case None => Left( err() )
}
case Left( e ) => Left( e )
}
}
def loginDevice(deviceSerialNumber: String, login: String, password: String): Either[Error, LocationResponse] = {
try {
db withSession { implicit session =>
val deviceRowOption = devices.filter(d => d.serialNumber === deviceSerialNumber).map(d => (d.id, d.currentLocationId.?, d.serialNumber.?)).firstOption
val locationRowEither = eitherMe(
Right( deviceRowOption ),
() => { DeviceNotExistError },
deviceRow => {
val locationRowOption = locations.filter(l => l.id === deviceRow._2.getOrElse(0L) && l.login === login && l.password === password).firstOption
Right( locationRowOption )
}
)
val programRowEither = eitherMe(
locationRowEither,
() => { IncorrectLoginOrPasswordError },
locationRow => {
val programRowOption = programs.filter(p => p.id === locationRow.programId).firstOption
Right( programRowOption )
}
)
val locationResponseEither = eitherMe(
programRowEither,
() => { ProgramNotExistError },
programRow => {
val program = Program(programRow.name, programRow.logo, programRow.moneyLevel, programRow.pointsForLevel,
programRow.description, programRow.rules, programRow.dailyCustomerScansLimit)
val locationData = LocationData(program)
val locationResponse = LocationResponse("access_token", System.currentTimeMillis(), locationData)
Right(locationResponse)
}
)
locationResponseEither
}
} catch {
case ex: SQLException =>
Left(DatabaseError)
}
}
答案 2 :(得分:0)
对我来说,当我有时无法避免嵌套复杂性时,我会提取出有意义的代码部分,并将其转换为新方法并赋予其有意义的名称。这将记录代码并使其更具可读性,并降低每个单独方法的复杂性。通常,一旦我这样做了,我就能够更好地看到流程,并且可以重构它以使其更有意义(在首次编写测试以涵盖我想要的行为之后)。
e.g。对于你的代码,你可以这样做:
class LocationDao {
val db = DbProvider.db
// Database tables
val devices = Devices.devices
val locations = Locations.locations
val programs = Programs.programs
val accessTokens = AccessTokens.accessTokens
def loginDevice(deviceSerialNumber: String, login: String, password: String): Either[Error, LocationResponse] = {
try {
db withSession { implicit session =>
checkDeviceRowOption(deviceSerialNumber, login, password)
}
} catch {
case ex: SQLException =>
Left(DatabaseError)
}
}
def checkDeviceRowOption(deviceSerialNumber: String, login: String, password: String): Either[Error, LocationResponse] = {
val deviceRowOption = devices.filter(d => d.serialNumber === deviceSerialNumber).map(d => (d.id, d.currentLocationId.?, d.serialNumber.?)).firstOption
deviceRowOption match {
case Some(deviceRow) => {
val locationRowOption = locations.filter(l => l.id === deviceRow._2.getOrElse(0L) && l.login === login && l.password === password).firstOption
locationRowOption match {
case Some(locationRow) => { checkProgramRowOption(locationRow) }
case None => Left(IncorrectLoginOrPasswordError)
}
}
case None => Left(DeviceNotExistError)
}
}
def checkProgramRowOption(locationRow: LocationRowType): Either[Error, LocationResponse] = {
val programRowOption = programs.filter(p => p.id === locationRow.programId).firstOption
programRowOption match {
case Some(programRow) => {
val program = Program(programRow.name, programRow.logo, programRow.moneyLevel, programRow.pointsForLevel,
programRow.description, programRow.rules, programRow.dailyCustomerScansLimit)
val locationData = LocationData(program)
val locationResponse = LocationResponse("access_token", System.currentTimeMillis(), locationData)
Right(locationResponse)
}
case None => Left(ProgramNotExistError)
}
}
}
请注意,这只是一个示例,可能无法编译,因为我没有您的lib,但您应该能够调整代码以便编译。