Java >> Java Program >  >> Java

Java-anteckningar är ett stort misstag

Anteckningar introducerades i Java 5, och vi blev alla glada. Så bra instrument för att göra koden kortare! Inga fler Hibernate/Spring XML-konfigurationsfiler! Bara anteckningar, precis där i koden där vi behöver dem. Inga fler markörgränssnitt, bara en anteckning som kan upptäckas av reflektion som behålls i körtid! Jag var också exalterad. Dessutom har jag skapat några bibliotek med öppen källkod som använder anteckningar mycket. Ta till exempel jcabi-aspekter. Dock är jag inte upprymd längre. Dessutom tror jag att anteckningar är ett stort misstag i Java-design.

Lång historia kort, det finns ett stort problem med anteckningar – de uppmuntrar oss att implementera objektfunktionalitet utanför av ett föremål, vilket strider mot själva principen om inkapsling. Objektet är inte längre fast, eftersom dess beteende inte helt definieras av dess egna metoder – en del av dess funktionalitet stannar någon annanstans. Varför är det dåligt? Låt oss se några exempel.

@Inject

Säg att vi kommenterar en egenskap med @Inject :

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

Sedan har vi en injektor som vet vad som ska injiceras:

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

Nu gör vi en instans av klassen Books via behållaren:

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

Klassen Books har ingen aning om hur och vem som ska injicera en instans av klassen DB Gillar det. Detta kommer att hända bakom kulisserna och utanför dess kontroll. Injektionen kommer att göra det. Det kan se bekvämt ut, men denna attityd orsakar mycket skada på hela kodbasen. Kontrollen är förlorad (inte inverterad, men förlorad!). Objektet är inte längre ansvarigt. Den kan inte vara ansvarig för vad som händer med den.

Så här ska det istället göras:

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

Den här artikeln förklarar varför Dependency Injection behållare är en felaktig idé i första hand:Dependency Injection Containers är kodförorenare. Anteckningar provocerar oss i princip att tillverka behållarna och använda dem. Vi flyttar funktionalitet utanför våra objekt och lägger den i containrar eller någon annanstans. Det beror på att vi inte vill duplicera samma kod om och om igen, eller hur? Det är korrekt, duplicering är dåligt, men att riva ett föremål är ännu värre. Mycket värre. Detsamma gäller ORM (JPA/Hibernate), där annoteringar används aktivt. Kolla det här inlägget, det förklarar vad som är fel med ORM:ORM är ett stötande antimönster. Anteckningar i sig är inte den viktigaste motivationen, men de hjälper oss och uppmuntrar oss genom att slita isär föremål och förvara delar på olika platser. De är containrar, sessioner, chefer, controllers, etc.

@XmlElement

Så här fungerar JAXB när du vill konvertera din POJO till XML. Först bifogar du @XmlElement anteckning till 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;
  }
}

Sedan skapar du en marshaller och ber den att konvertera en instans av klassen Book till 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);

Vem skapar XML? Inte book . Någon annan, utanför klassen Book . Detta är väldigt fel. Istället är det så här det borde ha gjorts. Först, klassen som inte har någon aning om XML:

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

Sedan, dekoratören som skriver ut den till 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()
    );
  }
}

Nu, för att skriva ut boken i XML, gör vi följande:

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

XML-utskriftsfunktionen finns i XmlBook . Om du inte gillar dekorationsidén kan du flytta toXML() metod till DefaultBook klass. Det är inte viktigt. Det viktiga är att funktionaliteten alltid stannar där den hör hemma – inne i objektet. Endast objektet vet hur man skriver ut sig självt till XML. Ingen annan!

@RetryOnFailure

Här är ett exempel (från mitt eget bibliotek):

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

Efter kompileringen kör vi en så kallad AOP-vävare som tekniskt förvandlar vår kod till något sånt här:

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

Jag förenklade den faktiska algoritmen för att försöka igen ett metodsamtal vid misslyckande, men jag är säker på att du förstår idén. AspectJ, AOP-motorn, använder @RetryOnFailure anteckning som en signal, som informerar oss om att klassen måste slås in i en annan. Det här händer bakom kulisserna. Vi ser inte den kompletterande klassen, som implementerar algoritmen för att försöka igen. Men bytekoden som produceras av AspectJ-vävaren innehåller en modifierad version av klassen Foo .

Det är precis vad som är fel med detta tillvägagångssätt – vi ser inte och kontrollerar inte instansieringen av det kompletterande objektet. Objektkomposition, som är den viktigaste processen inom objektdesign, döljs någonstans bakom kulisserna. Du kan säga att vi inte behöver se det eftersom det är ett komplement. Jag håller inte med. Vi måste se hur våra föremål är sammansatta. Vi kanske inte bryr oss om hur de fungerar, men vi måste se hela kompositionsprocessen.

En mycket bättre design skulle se ut så här (istället för kommentarer):

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

Och sedan implementeringen av 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);
        }
      }
    );
  }
}

Och nu, implementeringen av 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();
  }
}

Är koden längre? Ja. Är det renare? Mycket mer. Jag ångrar att jag inte förstod det för två år sedan, när jag började jobba med jcabi-aspekter.

Summan av kardemumman är att anteckningar är dåliga. Använd dem inte. Vad ska användas istället? Objektsammansättning.

Vad kan vara värre än kommentarer? Konfigurationer. Till exempel XML-konfigurationer. Spring XML-konfigurationsmekanismer är ett perfekt exempel på hemsk design. Jag har sagt det många gånger förut. Låt mig upprepa det igen – Spring Framework är en av de sämsta mjukvaruprodukterna i Java-världen. Om du kan hålla dig borta från det kommer du att göra dig själv en stor tjänst.

Det ska inte finnas några "konfigurationer" i OOP. Vi kan inte konfigurera våra objekt om de är riktiga objekt. Vi kan bara instansiera dem. Och den bästa metoden för instansiering är operatorn new . Denna operatör är nyckelinstrumentet för en OOP-utvecklare. Att ta det ifrån oss och ge oss "konfigurationsmekanismer" är ett oförlåtligt brott.

  • Java-anteckningar är ett stort misstag (webinarium #14); 4 maj 2016; 744 visningar; 13 likes
  • Dependency Injection Container är en dålig idé (webinar #9); 1 december 2015; 1264 visningar; 19 likes
  • Varför är Getters-and-Setters ett antimönster? (webinarium #4); 1 juli 2015; 3095 visningar; 53 likes

Java-tagg