Java >> Programma Java >  >> Java

Il corso crash Java Fluent API Designer

Da quando Martin Fowler parla di interfacce fluide, le persone hanno iniziato a concatenare metodi dappertutto, creando API (o DSL) fluide per ogni possibile caso d'uso. In linea di principio, quasi tutti i tipi di DSL possono essere mappati su Java. Diamo un'occhiata a come questo può essere fatto

Regole DSL

I DSL (Domain Specific Languages) sono generalmente costruiti da regole che assomigliano più o meno a queste

1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD ]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

In alternativa, potresti anche dichiarare la tua grammatica in questo modo (come supportato da questo bel sito di Railroad Diagrams):

Grammar ::= ( 
  'SINGLE-WORD' | 
  'PARAMETERISED-WORD' '('[A-Z]+')' |
  'WORD1' 'OPTIONAL-WORD'? | 
  'WORD2' ( 'WORD-CHOICE-A' | 'WORD-CHOICE-B' ) | 
  'WORD3'+ 
)

In parole povere, hai una condizione o stato iniziale, da cui puoi scegliere alcune delle parole delle tue lingue prima di raggiungere una condizione o stato finale. È come una macchina a stati e può quindi essere disegnato in un'immagine come questa:

Implementazione Java di tali regole

Con le interfacce Java, è abbastanza semplice modellare il DSL di cui sopra. In sostanza, devi seguire queste regole di trasformazione:

  • Ogni “parola chiave” DSL diventa un metodo Java
  • Ogni “connessione” DSL diventa un'interfaccia
  • Quando hai una scelta "obbligatoria" (non puoi saltare la parola chiave successiva), ogni parola chiave di quella scelta è un metodo nell'interfaccia corrente. Se è possibile una sola parola chiave, esiste un solo metodo
  • Quando hai una parola chiave "opzionale", l'interfaccia corrente estende quella successiva (con tutte le sue parole chiave/metodi)
  • Quando hai una "ripetizione" di parole chiave, il metodo che rappresenta la parola chiave ripetibile restituisce l'interfaccia stessa, invece dell'interfaccia successiva
  • Ogni sottodefinizione DSL diventa un parametro. Ciò consentirà ricorsività

Nota, è anche possibile modellare il DSL sopra con classi anziché interfacce. Ma non appena vuoi riutilizzare parole chiave simili, l'ereditarietà multipla dei metodi potrebbe tornare molto utile e potresti semplicemente stare meglio con le interfacce.

Con queste regole impostate, puoi ripeterle a piacimento per creare DSL di complessità arbitraria, come jOOQ. Ovviamente dovrai implementare in qualche modo tutte le interfacce, ma questa è un'altra storia.

Ecco come le regole di cui sopra vengono tradotte in Java:

// Initial interface, entry point of the DSL
// Depending on your DSL's nature, this can also be a class with static
// methods which can be static imported making your DSL even more fluent
interface Start {
  End singleWord();
  End parameterisedWord(String parameter);
  Intermediate1 word1();
  Intermediate2 word2();
  Intermediate3 word3();
}

// Terminating interface, might also contain methods like execute();
interface End {
  void end();
}

// Intermediate DSL "step" extending the interface that is returned
// by optionalWord(), to make that method "optional"
interface Intermediate1 extends End {
  End optionalWord();
}

// Intermediate DSL "step" providing several choices (similar to Start)
interface Intermediate2 {
  End wordChoiceA();
  End wordChoiceB();
}

// Intermediate interface returning itself on word3(), in order to allow
// for repetitions. Repetitions can be ended any time because this 
// interface extends End
interface Intermediate3 extends End {
  Intermediate3 word3();
}

Con la grammatica sopra definita, ora possiamo usare questo DSL direttamente in Java. Ecco tutti i possibili costrutti:

Start start = // ...

start.singleWord().end();
start.parameterisedWord("abc").end();

start.word1().end();
start.word1().optionalWord().end();

start.word2().wordChoiceA().end();
start.word2().wordChoiceB().end();

start.word3().end();
start.word3().word3().end();
start.word3().word3().word3().end();

E la cosa migliore è che la tua DSL si compila direttamente in Java! Ottieni un parser gratuito. Puoi anche riutilizzare questo DSL in Scala (o Groovy) usando la stessa notazione, o leggermente diversa in Scala, omettendo i punti "." e parentesi “()”:

 val start = // ...

 (start singleWord) end;
 (start parameterisedWord "abc") end;

 (start word1) end;
 ((start word1) optionalWord) end;

 ((start word2) wordChoiceA) end;
 ((start word2) wordChoiceB) end;

 (start word3) end;
 ((start word3) word3) end;
 (((start word3) word3) word3) end;

Esempi del mondo reale

Alcuni esempi del mondo reale possono essere visti in tutta la documentazione e nella base di codice di jOOQ. Ecco un estratto da un post precedente di una query SQL piuttosto complessa creata con jOOQ:

create().select(
    r1.ROUTINE_NAME,
    r1.SPECIFIC_NAME,
    decode()
        .when(exists(create()
            .selectOne()
            .from(PARAMETERS)
            .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
            .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
            .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
                val("void"))
        .otherwise(r1.DATA_TYPE).as("data_type"),
    r1.NUMERIC_PRECISION,
    r1.NUMERIC_SCALE,
    r1.TYPE_UDT_NAME,
    decode().when(
    exists(
        create().selectOne()
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
        create().select(count())
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
    .as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()

Ecco un altro esempio da una libreria che mi sembra piuttosto interessante. Si chiama jRTF ed è usato per creare documenti RTF in Java in uno stile fluido:

rtf()
  .header(
    color( 0xff, 0, 0 ).at( 0 ),
    color( 0, 0xff, 0 ).at( 1 ),
    color( 0, 0, 0xff ).at( 2 ),
    font( "Calibri" ).at( 0 ) )
  .section(
        p( font( 1, "Second paragraph" ) ),
        p( color( 1, "green" ) )
  )
).out( out );

Riepilogo

Le API fluenti sono state un clamore negli ultimi 7 anni. Martin Fowler è diventato un uomo molto citato e ottiene la maggior parte dei crediti, anche se prima c'erano API fluenti. Una delle più antiche "API fluenti" di Java può essere vista in java.lang.StringBuffer, che consente di aggiungere oggetti arbitrari a una stringa. Ma il più grande vantaggio di un'API fluente è la sua capacità di mappare facilmente "DSL esterni" in Java e implementarli come "DSL interni" di complessità arbitraria.


Etichetta Java