2015-11-17 8 views
6

Spesso raccomando la trasformazione AST di Groovy @Immutable come un modo semplice per rendere le classi, bene, immutabili. Questo funziona sempre bene con altre classi di Groovy, ma qualcuno mi ha chiesto di recente se potessi mescolare quelle classi nel codice Java. Ho sempre pensato che la risposta fosse sì, ma sto colpendo un ostacolo.Groovy classi @Immutable in Java

Dire che ho un immutabile User classe:

import groovy.transform.Immutable 

@Immutable 
class User { 
    int id 
    String name 
} 

Se provo questo utilizzando un test JUnit scritto in Groovy, tutto funziona come previsto:

import org.junit.Test 

class UserGroovyTest { 

    @Test 
    void testMapConstructor() { 
     assert new User(name: 'name', id: 3) 
    } 

    @Test 
    void testTupleConstructor() { 
     assert new User(3, 'name') 
    } 

    @Test 
    void testDefaultConstructor() { 
     assert new User() 
    } 

    @Test(expected = ReadOnlyPropertyException) 
    void testImmutableName() { 
     User u = new User(id: 3, name: 'name') 
     u.name = 'other' 
    } 
} 

posso fare lo stesso con un JUnit test scritto in Java:

import static org.junit.Assert. *;

import org.junit.Test; 

public class UserJavaTest { 

    @Test 
    public void testDefaultCtor() { 
     assertNotNull(new User()); 
    } 

    @Test 
    public void testTupleCtor() { 
     assertNotNull(new User(3, "name")); 
    } 

    @Test 
    public void testImmutableName() { 
     User u = new User(3, "name"); 
     // u.setName("other") // Method not found; doesn't compile 
    } 
} 

Questo funziona, anche se ci sono problemi all'orizzonte. IntelliJ 15 non ama la chiamata a new User(), sostenendo che il costruttore non è stato trovato. Ciò significa anche che l'IDE sottolinea la classe in rosso, il che significa che ha un errore di compilazione. Il test passa comunque, il che è un po 'strano, ma così sia.

Se provo a utilizzare direttamente la classe User nel codice Java, le cose iniziano a diventare strane.

public class UserDemo { 
    public static void main(String[] args) { 
     User user = new User(); 
     System.out.println(user); 
    } 
} 

Ancora una volta IntelliJ non è felice, ma compila e gira. L'uscita è, di tutte le cose:

User(0) 

Questo è strano, perché anche se il @Immutable trasformazione non genera un metodo toString, io invece lo aspettavo l'uscita di mostrare entrambe le proprietà. Tuttavia, ciò potrebbe essere dovuto al fatto che la proprietà name è null, quindi non è inclusa nell'output.

Se cerco di utilizzare il costruttore tuple:

public class UserDemo { 
    public static void main(String[] args) { 
     User user = new User(3, "name"); 
     System.out.println(user); 
    } 
} 

ottengo

User(0, name) 

come l'uscita, almeno questa volta (a volte non funziona affatto).

Quindi ho aggiunto un file di build Gradle. Se metto le classi Groovy sotto src\main\groovy e le classi Java in src\main\java (lo stesso per i test, ma utilizzando la cartella test invece), immediatamente ottengo un problema di compilazione:

> gradle test 
error: cannot find symbol 
User user = new User(...) 
^ 

Io di solito risolvere i problemi di cross-compilazione come questo cercando di utilizzare il compilatore Groovy per tutto. Se metto entrambe le classi sotto src\main\java, non cambia nulla, che non è una grande sorpresa. Ma se ho messo entrambe le classi sotto src\main\groovy, allora ottengo questo durante la fase di compileGroovy:

> gradle clean test 
error: constructor in class User cannot be applied to the given types; 
User user = new User(3, "name"); 

required: no arguments 
found: int,String 
reason: actual and formal arguments differ in length 

Huh. Questa volta si oppone al costruttore di tuple, perché pensa che abbia solo un costruttore predefinito. So che la trasformazione aggiunge un predefinito, una mappa e un costruttore di tupla, ma forse non vengono generati in tempo perché il codice Java possa vederli.

Per inciso, se separare le classi Java e Groovy di nuovo, e aggiungere il seguente al mio Gradle costruire:

sourceSets { 
    main { 
     java { srcDirs = []} 
     groovy { srcDir 'src/main/java' } 
    } 
} 

ottengo lo stesso errore. Se non aggiungo il blocco sourceSets, ottengo l'errore di classe User non trovato in precedenza.

Quindi la linea di fondo è, qual è il modo corretto per aggiungere una classe @Immutable Groovy a un sistema Java esistente? C'è un modo per generare i costruttori in tempo perché Java li veda?

Ho fatto presentazioni Groovy agli sviluppatori Java per anni e ho detto che si può fare questo, solo per ora incorrere in problemi. Per favore aiutami a salvare la faccia in qualche modo. :)

+0

Gli IDE tendono a dare falsi negativi quando si tratta di Groovy, specialmente con le trasformazioni AST. Per il problema della compilazione incrociata potresti provare a creare due progetti separati, uno java e uno groovy. Se questa è ancora una domanda irrisolta, gli darò un vortice al mattino. Questo sembra davvero dovrebbe funzionare. – cjstehno

+0

Il comportamento è lo stesso con '@ CompileStatic'? – dmahapatro

+0

'@ CompileStatic' non modifica nulla – kousen

risposta

4

Ho provato anche il tuo scenario, dove hai un singolo progetto con una directory src/main/java e src/main/groovy e ho finito con errori di compilazione simili a quello che hai visto.

Sono stato in grado di utilizzare gli oggetti immutabili di Groovy in Java quando ho inserito gli immutabili Groovy in un progetto separato dal codice Java. Ho creato un semplice esempio e l'ho inviato a GitHub (https://github.com/cjstehno/immut).

Fondamentalmente è un multiprogetto Gradle con tutto il codice Groovy (l'oggetto immutabile) nel sottoprogetto immut-groovy e tutto il codice Java nel progetto immut-java. Il progetto immut-java dipende dal progetto immut-groovy e utilizza l'Something oggetto immutabile:

public class SomethingFactory { 

    Something createSomething(int id, String label){ 
     return new Something(id, label); 
    } 
} 

ho aggiunto un test di unità nel progetto Java che crea una nuova istanza della classe Groovy immutabile e verifica dei suoi contenuti.

public class SomethingFactoryTest { 
    @Test 
    public void createSomething(){ 
     Something something = new SomethingFactory().createSomething(42, "wonderful"); 

     assertEquals(something.getId(), 42); 
     assertEquals(something.getLabel(), "wonderful"); 
    } 
} 

Questo non è proprio l'ideale, ma funziona.