2015-02-25 12 views
5

Cerco di semplificare il processo di convalida per l'elaborazione delle risposte per le richieste HTTP in Spray (utilizzo Slick per l'accesso al database). Attualmente, controllo in una query se dovrei andare oltre alla seguente query o no (errore di ritorno). Questo finisce con la corrispondenza del modello nidificato. Ogni caso di convalida può restituire un errore diverso, quindi non posso usare nessun flatMap.Scala: evitare la combinazione di motivi nidificati troppo complessi

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) 
    } 
    } 
} 

Qual è un buon modo per semplificare questo? Forse c'è altro approccio ..

+2

L'esempio del codice è troppo specifico per quello che stai facendo, cerca di essere più generico. Ma per la tua domanda se cambi i tuoi metodi xxxRowOption per restituire 'O 'sarai in grado di usarlo per la comprensione. – Maxim

+0

Usa una comprensione. –

+0

Ho rifattorizzato questo codice con la comprensione preliminare. Grazie. – piobab

risposta

6

In generale, è possibile utilizzare un per-la comprensione di concatenare un sacco di strutture monadici avete qui (compreso Try, Option, e Either) senza nidificazione. Per esempio:

for { 
    val1 <- Try("123".toInt) 
} yield for { 
    val2 <- Some(val1).map(_ * 2) 
    val3 = Some(val2 - 55) 
    val4 <- val3 
} yield val4 * 2 

nel vostro stile, questo potrebbe altrimenti hanno guardato come:

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 
    } 
} 
+0

Questo caso è errato: 'caso Errore => Nessuno'. Utilizzare 'case f: Failure => None' o' case Failure (_) => None'. – jrudolph

+0

hai ragione - mio male. Non ho eseguito nessuno di questi codici, stavo solo cercando di dare il sapore delle incomprensioni contro partite e mappe annidate. Sentiti libero di modificare la risposta se noti altri piccoli errori. –

1

È possibile definire un metodo di supporto per il Eiter ising vostro flusso di controllo.

Il vantaggio di fare questo è che avrete un grande controllo e flessibilità nel flusso.

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) 
    } 
} 
+0

Ho preso un diverso approccio con la comprensione. Penso che questa soluzione sia buona, ma cosa succede se vogliamo restituire più di un errore? – piobab

+0

Per le compressioni si ha il problema della buona gestione degli errori. In questo caso puoi gestire i tuoi errori come preferisci. A partire da ora ... questo codice restituirà diversi errori per problemi diversi esattamente come il tuo codice originale. –

0

Per quanto mi riguarda, quando a volte non posso evitare di complessità nidificato, vorrei estrarre fuori sezione del codice che ha senso insieme e farne un nuovo metodo e dargli un nome significativo. Ciò documenterà il codice e lo renderà più leggibile e ridurrà la complessità all'interno di ogni singolo metodo. E di solito, una volta fatto ciò, sono in grado di vedere meglio il flusso e potrei essere in grado di rifattorizzarlo per avere più senso (dopo aver prima scritto i test per coprire il comportamento che voglio).

ad es. per il codice, si può fare qualcosa di simile:

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) 
    } 
    } 

} 

Si noti che questo è solo un esempio e probabilmente non verrà compilato perché non ho il tuo lib, ma si dovrebbe essere in grado di ottimizzare il codice per per compilare.