2011-12-30 14 views
6

Ho letto sull'approccio "interfaccia fluida" OO in Java, JavaScript e Scala e mi piace l'aspetto di esso, ma ho faticato a vedere come riconciliarlo con un approccio più basato su tipo/funzionale in Scala .Come posso combinare interfacce fluenti con uno stile funzionale in Scala?

Per fare un esempio molto specifico di quello che voglio dire: ho scritto un client API che può essere invocato in questo modo:

val response = MyTargetApi.get("orders", 24) 

Il valore restituito da get() è un tipo Tuple3 chiamato RestfulResponse, come definito nel mio package object:

// 1. Return code 
// 2. Response headers 
// 2. Response body (Option) 
type RestfulResponse = (Int, List[String], Option[String]) 

Questo funziona bene - e io in realtà non vogliono sacrificare la semplicità funzionale di un valore di ritorno tuple - ma vorrei estendere la libreria con varie f' 'chiamate di metodo, forse qualcosa di simile:

val response = MyTargetApi.get("customers", 55).throwIfError() 
// Or perhaps: 
MyTargetApi.get("orders", 24).debugPrint(verbose=true) 

Come posso combinare la semplicità funzionale di get() restituire una tupla digitato (o simili) con la possibilità di aggiungere piu 'luent capacità fluente' alla mia API?

+0

Questo approccio "interfaccia fluida" ... non sembra molto amichevole verso linguaggi tipizzati staticamente. Anche se sono sicuro che con un po 'di codice ed ereditarietà di wrapping potresti probabilmente farlo con Scala, non sarà sicuro come sicuro come avere getOrders' e 'getCustomers' separatamente, invece di' get ("orders") 'e 'get (" customers ")' usando lo stesso metodo 'get'. –

+0

Grazie Dan - ma non mi preoccuperei troppo della sintassi 'get (" slug ", id)' - questa non è veramente la mia domanda. In ogni caso nella libreria c'è un'altra modalità più tipicamente simile a 'MyTargetApi.orders.get (id)' –

+0

Personalmente penso che dovresti offrire un esempio più rappresentativo di un codice scorrevole e esattamente quale bit pensi non sia funzionante. Al momento, sembra proprio dalla tua domanda che non sai veramente cosa significhi fluente –

risposta

7

Sembra che tu abbia a che fare con un'API lato client di una comunicazione di stile di riposo. Il tuo metodo get sembra essere quello che attiva l'effettivo ciclo di richiesta/risposta. Sembra che si avrebbe a che fare con questo:

  • proprietà del trasporto (come credenziali, livello di debug, la gestione degli errori)
  • dati che forniscono per l'ingresso (il tuo id e tipo di Record (ordine o cliente)
  • fare qualcosa con i risultati

credo che per le proprietà del trasporto, si può mettere un po 'di esso nel costruttore della MyTargetApi obj ect, ma è anche possibile creare un interrogazione oggetto che memorizzerà quelli per una singola query e può essere impostato in un fluente modo utilizzando un metodo query():

MyTargetApi.query().debugPrint(verbose=true).throwIfError() 

Questo sarebbe tornare un stateful Query oggetto che memorizza il valore per il livello di registro, gestione degli errori.Per fornire i dati per l'ingresso, è anche possibile utilizzare l'oggetto query per impostare questi valori, ma invece di restituire la vostra risposta restituire un QueryResult:

class Query { 
    def debugPrint(verbose: Boolean): this.type = { _verbose = verbose; this } 
    def throwIfError(): this.type = { ... } 
    def get(tpe: String, id: Int): QueryResult[RestfulResponse] = 
    new QueryResult[RestfulResponse] { 
     def run(): RestfulResponse = // code to make rest call goes here 
    } 
} 

trait QueryResult[A] { self => 
    def map[B](f: (A) => B): QueryResult[B] = new QueryResult[B] { 
    def run(): B = f(self.run()) 
    } 
    def flatMap[B](f: (A) => QueryResult[B]) = new QueryResult[B] { 
    def run(): B = f(self.run()).run() 
    } 
    def run(): A 
} 

Poi finalmente per ottenere i risultati che chiamate run. Così, alla fine della giornata si può chiamare in questo modo:

MyTargetApi.query() 
    .debugPrint(verbose=true) 
    .throwIfError() 
    .get("customers", 22) 
    .map(resp => resp._3.map(_.length)) // body 
    .run() 

Che dovrebbe essere una richiesta dettagliata che errore fuori il problema, recuperare i clienti con id 22, mantenere il corpo e ottenere la sua lunghezza come Option[Int].

L'idea è che è possibile utilizzare map per definire calcoli su un risultato che non si ha ancora. Se aggiungiamo flatMap ad esso, allora potresti anche combinare due calcoli da due diverse query.

+0

Wow - enorme grazie ** huynhjl ** per aver superato il povero fraseggio della mia domanda e aver messo insieme questa risposta. È estremamente utile: mostra come definire un'interfaccia fluente che può restituire il mio tipo 'RestfulResponse' o tramite' map' può applicare un ulteriore calcolo e restituirlo. Posso confermare che il metodo originale 'query()' che menzioni fa parte di 'MyTargetApi' e restituisce semplicemente un' nuovo oggetto Query() '? Inoltre sarebbe troppo chiedere di vedere la definizione di 'flatMap'? Grazie ancora! –

+1

@AlexDean, ho aggiunto 'flatMap'. Sì 'query()' sarebbe parte dell'originale 'MyTargetApi' e restituirà un nuovo oggetto' Query'. Per favore, usa la mia risposta solo per alcune idee. Vi invito anche a guardare http://engineering.foursquare.com/2011/01/21/rogue-a-type-safe-scala-dsl-for-querying-mongodb/ e in generale qualsiasi interfaccia ORM e nosql o wrapper scritto per Scala per più ispirazione. – huynhjl

+0

Molte grazie ** huynhjl **. Ho usato la fonte [Squeryl] (https://github.com/max-l/Squeryl/tree/master/src/main/scala/org/squeryl) per l'ispirazione, ma controllerò sicuramente Rogue e anche alcuni degli altri strumenti ORM/NoSQL ... –

3

Per essere onesti, penso che tu abbia bisogno di sentirti ancora un po 'di più perché l'esempio non è ovviamente funzionale, né particolarmente scorrevole. Sembra che si possa confondere la fluidità con non idempotente nel senso che il proprio metodo debugPrint sta presumibilmente eseguendo I/O e il throwIfError genera eccezioni. È questo che vuoi dire?

Se ci si riferisce a se un costruttore di stato è funzionale, la risposta è "non nel senso più puro". Tuttavia, tieni presente che un builder non deve avere lo stato.

case class Person(name: String, age: Int) 

In primo luogo; questo può essere creato utilizzando i parametri denominati:

Person(name="Oxbow", age=36) 

Oppure, un costruttore senza stato:

object Person { 
    def withName(name: String) 
    = new { def andAge(age: Int) = new Person(name, age) } 
} 

Hey presto:

scala> Person withName "Oxbow" andAge 36 

Per quanto riguarda l'utilizzo di stringhe senza tipo per definire la query stanno facendo; questa è una forma scadente in un linguaggio tipizzato staticamente.Per di più, non c'è bisogno:

sealed trait Query 
case object orders extends Query 

def get(query: Query): Result 

Hey presto:

api get orders 

Anche se, penso che questa sia una cattiva idea - non si dovrebbe avere un unico metodo che può dare indietro fittiziamente completamente diversi tipi di risultati


Per concludere: io personalmente penso che ci sia alcun motivo che scorrevolezza e funzionale non può mescolare, in quanto funzionale solo indica la mancanza di stato mutabile un ND la forte preferenza per le funzioni idempotenti per eseguire la logica in

Ecco uno per voi:.

args.map(_.toInt) 

args map toInt 

direi che il secondo è più fluente. È possibile se si definisce:

val toInt = (_ : String).toInt 

Cioè; se si definisce una funzione. Trovo che funzioni e fluidità si mescolino molto bene in Scala.

+0

Ciao ** oxbow_lakes ** - molte molte grazie per aver dedicato del tempo a mettere insieme questa risposta. Avevi ragione - il fraseggio della mia domanda era molto povero, scuse per tutta la confusione. Sono d'accordo sul fatto che siano funzionali e fluenti completamente compatibili - e ti ringraziamo per gli esempi che riguardano i costruttori statali e apolidi. Sulle stringhe non tipizzate per le risorse HTTP 'get()' - sono d'accordo, è stata una cattiva idea, ho intenzione di rimuovere quella capacità dall'API (o almeno di contrassegnarla come 'non sicura', in stile Haskell). –

0

Si potrebbe provare con get() restituisce un oggetto wrapper che potrebbe assomigliare a questo

type RestfulResponse = (Int, List[String], Option[String]) 

class ResponseWrapper(private rr: RestfulResponse /* and maybe some flags as additional arguments, or something? */) { 

    def get : RestfulResponse = rr 

    def throwIfError : RestfulResponse = { 
     // Throw your exception if you detect an error 
     rr // And return the response if you didn't detect an error 
    } 

    def debugPrint(verbose: Boolean, /* whatever other parameters you had in mind */) { 
     // All of your debugging printing logic 
    } 

    // Any and all other methods that you want this API response to be able to execute 

} 

Fondamentalmente, questo ti permette di mettere la vostra risposta in un contenere che ha tutte queste belle metodi che si desidera e, se vuoi semplicemente ottenere la risposta avvolta, puoi semplicemente chiamare il metodo get() del wrapper.

Ovviamente, il lato negativo di questo è che sarà necessario modificare un po 'l'API, se questo è preoccupante per te. Beh ... probabilmente potresti evitare di dover cambiare la tua API, in realtà, se tu, invece, hai creato una conversione implicita da RestfulResponse a ResponseWrapper e viceversa. È qualcosa che vale la pena considerare.

+0

E in che modo questo è lo "stile funzionale" che l'OP chiedeva? Stai lanciando errori e eseguendo I/O –

+1

@oxbox_lakes Sei corretto; non è funzionale, ma la maggior parte della programmazione pratica deve scendere a compromessi per quanto riguarda il funzionalismo. Penso che questa sia la soluzione migliore per fare ciò che sta chiedendo. Io, personalmente, consiglierei di cambiare l'API, ma non è la mia chiamata da fare. Se vuole lanciare errori ed eseguire I/O, chi sono io per dire che non dovrebbe? – Destin

+0

Ma era in particolare la sua domanda: "come si fa un mix fluente e funzionale?". Quale non hai in alcun modo risposto. –