Java >> Programma Java >  >> Java

Le annotazioni Java sono un grosso errore

Le annotazioni sono state introdotte in Java 5 e ci siamo tutti emozionati. Un ottimo strumento per accorciare il codice! Niente più file di configurazione XML di Hibernate/Spring! Solo annotazioni, proprio lì nel codice dove ne abbiamo bisogno. Niente più interfacce marker, solo un'annotazione rilevabile dalla riflessione conservata in runtime! Anch'io ero eccitato. Inoltre, ho creato alcune librerie open source che utilizzano pesantemente le annotazioni. Prendi jcabi-aspects, per esempio. Tuttavia, non sono più entusiasta. Inoltre, credo che le annotazioni siano un grosso errore nella progettazione di Java.

Per farla breve, c'è un grosso problema con le annotazioni:ci incoraggiano a implementare la funzionalità degli oggetti all'esterno di un oggetto, il che è contrario al principio stesso dell'incapsulamento. L'oggetto non è più solido, poiché il suo comportamento non è definito interamente dai suoi metodi:alcune delle sue funzionalità rimangono altrove. Perché è cattivo? Vediamo in alcuni esempi.

@Inject

Supponiamo di annotare una proprietà con @Inject :

import javax.inject.Inject;
public class Books {
  @Inject
  private final DB db;
  // some methods here, which use this.db
}

Poi abbiamo un iniettore che sa cosa iniettare:

Injector injector = Guice.createInjector(
  new AbstractModule() {
    @Override
    public void configure() {
      this.bind(DB.class).toInstance(
        new Postgres("jdbc:postgresql:5740/main")
      );
    }
  }
);

Ora stiamo creando un'istanza della classe Books tramite il contenitore:

Books books = injector.getInstance(Books.class);

La classe Books non ha idea di come e chi inietterà un'istanza della classe DB dentro. Questo accadrà dietro le quinte e al di fuori del suo controllo. L'iniezione lo farà. Può sembrare conveniente, ma questo atteggiamento causa molti danni all'intera base di codice. Il controllo è perso (non invertito, ma perso!). L'oggetto non è più in carica. Non può essere responsabile di ciò che gli sta accadendo.

Invece, ecco come dovrebbe essere fatto:

class Books {
  private final DB db;
  Books(final DB base) {
    this.db = base;
  }
  // some methods here, which use this.db
}

Questo articolo spiega perché i contenitori di iniezione di dipendenza sono un'idea sbagliata in primo luogo:i contenitori di iniezione di dipendenza sono inquinatori di codice. Le annotazioni fondamentalmente ci provocano a creare i contenitori e ad usarli. Spostiamo la funzionalità al di fuori dei nostri oggetti e la mettiamo in contenitori o da qualche altra parte. Questo perché non vogliamo duplicare lo stesso codice più e più volte, giusto? Esatto, la duplicazione è negativa, ma fare a pezzi un oggetto è anche peggio. Molto peggio. Lo stesso vale per ORM (JPA/Hibernate), dove le annotazioni vengono utilizzate attivamente. Controlla questo post, spiega cosa c'è di sbagliato in ORM:ORM è un anti-modello offensivo. Le annotazioni da sole non sono la motivazione chiave, ma ci aiutano e ci incoraggiano facendo a pezzi gli oggetti e mantenendo le parti in luoghi diversi. Sono contenitori, sessioni, gestori, controller, ecc.

@XmlElement

Ecco come funziona JAXB, quando vuoi convertire il tuo POJO in XML. Innanzitutto, alleghi il @XmlElement annotazione al getter:

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Book {
  private final String title;
  public Book(final String title) {
    this.title = title;
  }
  @XmlElement
  public String getTitle() {
    return this.title;
  }
}

Quindi, crei un marshaller e gli chiedi di convertire un'istanza della classe Book in XML:

final Book book = new Book("0132350882", "Clean Code");
final JAXBContext ctx = JAXBContext.newInstance(Book.class);
final Marshaller marshaller = ctx.createMarshaller();
marshaller.marshal(book, System.out);

Chi sta creando l'XML? Non il book . Qualcun altro, al di fuori della classe Book . Questo è molto sbagliato. Invece, è così che avrebbe dovuto essere fatto. Innanzitutto, la classe che non ha idea di XML:

class DefaultBook implements Book {
  private final String title;
  DefaultBook(final String title) {
    this.title = title;
  }
  @Override
  public String getTitle() {
    return this.title;
  }
}

Quindi, il decoratore che lo stampa nell'XML:

class XmlBook implements Book{
  private final Book origin;
  XmlBook(final Book book) {
    this.origin = book;
  }
  @Override
  public String getTitle() {
    return this.origin.getTitle();
  }
  public String toXML() {
    return String.format(
      "<book><title>%s</title></book>",
      this.getTitle()
    );
  }
}

Ora, per stampare il libro in XML, procediamo come segue:

String xml = new XmlBook(
  new DefaultBook("Elegant Objects")
).toXML();

La funzionalità di stampa XML è all'interno di XmlBook . Se non ti piace l'idea del decoratore, puoi spostare il toXML() metodo al DefaultBook classe. Non è importante. L'importante è che la funzionalità rimanga sempre al suo posto, all'interno dell'oggetto. Solo l'oggetto sa come stampare se stesso sull'XML. Nessun altro!

@RetryOnFailure

Ecco un esempio (dalla mia libreria):

import com.jcabi.aspects.RetryOnFailure;
class Foo {
  @RetryOnFailure
  public String load(URL url) {
    return url.openConnection().getContent();
  }
}

Dopo la compilazione, eseguiamo un cosiddetto tessitore AOP che trasforma tecnicamente il nostro codice in qualcosa del genere:

class Foo {
  public String load(URL url) {
    while (true) {
      try {
        return _Foo.load(url);
      } catch (Exception ex) {
        // ignore it
      }
    }
  }
  class _Foo {
    public String load(URL url) {
      return url.openConnection().getContent();
    }
  }
}

Ho semplificato l'algoritmo vero e proprio per riprovare una chiamata al metodo in caso di errore, ma sono sicuro che avrai l'idea. AspectJ, il motore AOP, utilizza @RetryOnFailure annotazione come segnale, informandoci che la classe deve essere avvolta in un'altra. Questo sta accadendo dietro le quinte. Non vediamo quella classe supplementare, che implementa l'algoritmo di ripetizione dei tentativi. Ma il bytecode prodotto da AspectJ weaver contiene una versione modificata della classe Foo .

Questo è esattamente ciò che non va in questo approccio:non vediamo e non controlliamo l'istanziazione di quell'oggetto supplementare. La composizione degli oggetti, che è il processo più importante nella progettazione degli oggetti, è nascosta da qualche parte dietro le quinte. Potresti dire che non abbiamo bisogno di vederlo poiché è supplementare. Non sono d'accordo. Dobbiamo vedere come sono composti i nostri oggetti. Forse non ci interessa come funzionano, ma dobbiamo vedere l'intero processo di composizione.

Un design molto migliore sarebbe simile a questo (invece delle annotazioni):

Foo foo = new FooThatRetries(new Foo());

E poi, l'implementazione di FooThatRetries :

class FooThatRetries implements Foo {
  private final Foo origin;
  FooThatRetries(Foo foo) {
    this.origin = foo;
  }
  public String load(URL url) {
    return new Retry().eval(
      new Retry.Algorithm<String>() {
        @Override
        public String eval() {
          return FooThatRetries.this.load(url);
        }
      }
    );
  }
}

E ora, l'implementazione di Retry :

class Retry {
  public <T> T eval(Retry.Algorithm<T> algo) {
    while (true) {
      try {
        return algo.eval();
      } catch (Exception ex) {
        // ignore it
      }
    }
  }
  interface Algorithm<T> {
    T eval();
  }
}

Il codice è più lungo? Sì. È più pulito? Molto più. Mi dispiace di non averlo capito due anni fa, quando ho iniziato a lavorare con jcabi-aspects.

La linea di fondo è che le annotazioni sono cattive. Non usarli. Cosa dovrebbe essere usato invece? Composizione dell'oggetto.

Cosa potrebbe esserci di peggio delle annotazioni? Configurazioni. Ad esempio, le configurazioni XML. I meccanismi di configurazione Spring XML sono un perfetto esempio di design terribile. L'ho già detto molte volte. Lascia che lo ripeta ancora:Spring Framework è uno dei peggiori prodotti software nel mondo Java. Se riesci a starne lontano, ti farai un grande favore.

Non dovrebbero esserci "configurazioni" in OOP. Non possiamo configurare i nostri oggetti se sono oggetti reali. Possiamo solo istanziarli. E il miglior metodo di istanziazione è l'operatore new . Questo operatore è lo strumento chiave per uno sviluppatore OOP. Togliercelo e darci “meccanismi di configurazione” è un crimine imperdonabile.

  • Le annotazioni Java sono un grosso errore (webinar n. 14); 4 maggio 2016; 744 visualizzazioni; 13 Mi piace
  • Il contenitore di iniezione di dipendenza è una cattiva idea (webinar n. 9); 1 dicembre 2015; 1264 visualizzazioni; 19 Mi piace
  • Perché Getters-and-Setter è un anti-pattern? (webinar n. 4); 1 luglio 2015; 3095 visualizzazioni; 53 Mi piace

Etichetta Java