[JAVA] Warum Dependency Injection nicht so schlecht ist

Magst du funktionale Programmierung? Ich hasse es. Ich programmiere gerne mit unveränderlichen Objekten ohne Nebenwirkungen, aber ich hasse funktionale Programmierung. Ich denke, Sie sollten keine funktionale Programmierung verwenden, um zu zeigen, dass Sie klug genug sind, um schwierige Techniken anzuwenden. Unabhängig davon, ob es sich um funktionale Programmierung oder prozedurale Programmierung handelt, ist es wichtig, Code zu schreiben, der leicht zu verstehen und zu warten ist.

Ist es nicht die Ursache für die Arroganz wie "für das Verbot von Strafen", dass das technische oberste Prinzip, dass die Person, die die schwierige Technik kennt, großartig ist und die Person, die den esoterischeren Code schreibt, großartig ist? Ich denke, dass der Pfad, dem Entwurfsmuster einmal gefolgt sind der funktionalen Programmierung folgt.

xkcd

Warum die Abhängigkeitsinjektion schlecht ist

(Der Code in diesem Abschnitt basiert auf diesem Video)

Sie zielten auf DI-Container als nächstes Ziel.

In der funktionalen Programmierung sollten Funktionen rein sein. Sie müssen für eine Eingabe immer dieselbe Ausgabe zurückgeben. Der Zweck des IoC-Containers besteht in erster Linie darin, die interne Implementierung eines Moduls von außen flexibel umschreiben zu können. Erstens steht es im Widerspruch zum Zweck der funktionalen Programmierung.

@ApplicationScoped
public class BusinessLogic{

    private final Config config;

    @Inject
    public BusinessLogic( Config config ) {
        this.config = config;
    }

    public Name readName() {
        var parts = config.getName().split( " " );
        return new Name( parts[0], parts[1] );
    }

    public Age readAge() {
        return new Age( config.getAge() );
    }

    public Person readPerson() {
        return new Person( readName(), readAge() );
    }
}

Jede Methode hängt von Config ab. Config wird nicht direkt als Methodenargument übergeben, sondern indirekt vom DI-Container abgerufen. Unreine Funktionen führen zu Nebenwirkungen. Die Nebenwirkungen sind nicht gut, oder?

Dann muss die indirekte Abhängigkeit direkt gemacht werden. Sie müssen lediglich alle Abhängigkeiten direkt als Argumente übergeben. Verschieben wir die Erfassung der "Config" -Instanz, die dem DI-Container überlassen wurde, in das Argument.

def readName(config: Config): Name = {
    val parts = config.name.split(" ")
    Name(parts(0), parts(1))
}

Theoretisch ist das in Ordnung, aber die eigentliche Anwendung ist voller Abhängigkeiten. Einstellungen, Benutzerinformationen, Sitzungen, DB-Zugriff, Cache-Server-Aufrufe, externe APIs, Nachrichtenwarteschlangen ... Wenn Sie es versuchen, erhalten Sie eine Menge Kesselplatten, die für Java nicht lächerlich sein können, mit einer Menge Argumente pro Funktion (und viele argumentieren immer noch, dass dies getan werden sollte). Ich bin sicher).

Eine übliche Lösung für dieses Problem ist die Verwendung einer Leitermonade oder einer Staatsmonade. Monaden ermöglichen es Funktionen, rein zu bleiben und auf den Kontext zuzugreifen.

(def read-person
  (domonad reader-m  ; wtf is this?
    [name read-name
     age read-age]
    {:name name :age age}))

Jetzt hast du einen schönen Code. Ich bin glücklich.

Kontext

Wie viel besser ist Code mit Monaden als Code mit IoC-Containern? Die Klarheit ist subjektiv, aber "Ich muss Monaden für alles verwenden!" Ich denke nicht, dass es überwältigend leicht zu verstehen ist. Lohnt es sich, Monad vorzustellen, nur weil Sie möchten, dass die Funktion rein ist?

In jedem Fall können Sie bei so vielen Artikeln über "Was ist eine Monade?" Daraus schließen, dass Monaden weder einfach noch unkompliziert sind (es gibt viele objektorientierte Artikel und Kommentare). Es mag argumentiert werden, aber die Objektorientierung ist auch schwierig genug (ich weiß immer noch nicht, wie man Klassen in Java richtig erbt). Und der schwierige Teil ist an sich schlecht. Es schafft ein viel größeres Problem als das, das Monad zu lösen versuchte.

Das Problem bestand in erster Linie darin, die Abhängigkeit zu beseitigen und in der Funktion auszudrücken. Aber hier kommt eine Frage. Warum wurde "Config" überhaupt nicht als Parameter im Programmiermodell mit DI übergeben?

Die Antwort ist einfach, da Config kein Parameter ist. Weil es ein ** Kontext ** bei der Ausführung der Anwendung ist. Bei der Parameterübergabemethode bestimmt der * Aufrufer * den Kontext des Kontexts (es ist natürlich der Aufrufer, der entscheidet, was im Argument übergeben wird). Im DI-Container sind weder der Anrufer noch der Anrufer für den Kontext verantwortlich. Genau dafür ist der DI-Container verantwortlich. Dies ist genau der Nachteil des Versuchs, alles in Parametern zu übergeben, weshalb viele Funktionen Parameter haben, die sie nicht selbst verwenden müssen.

Anführer Monade

  1. Leader-Monade ist eine übliche Methode, um den Kontext weiterzugeben
  2. Wickeln Sie einen impliziten Lesevorgang grundsätzlich in eine Monade ein
  3. Vorteil: Lesen wird als Typ abstrahiert
  4. Aber ich glaube, das ist, als würde man eine Spinne mit einer Kanone schlagen
  5. Bei Monaden geht es um Sequenzierung, nicht um das Übergeben von Kontext

Warum die Abhängigkeitsinjektion nicht so schlecht ist

Um den vorherigen Abschnitt zusammenzufassen, der Kontext ist keine Eingabe, daher wird der Kontext nicht als Argument übergeben. Die Abhängigkeitsinjektion übergibt ein Argument ist ** nicht **. Wenn Sie den Kontext aus den Parametern kombinieren und als Eingabe der Funktion betrachten, ist es schwierig zu erkennen, welcher der Kontext und welcher der Parameter ist. Monaden sind eine Möglichkeit, den Kontext zu abstrahieren und von den Parametern zu trennen, jedoch auf Kosten der Komplexität der Monaden selbst.

Und vor allem besteht das Problem der Ablehnung von Abhängigkeiten darin, dass wir, anstatt das ursprüngliche Problem des Umgangs mit "Kontext" anzugehen, der Meinung sind, dass alle Probleme durch funktionale Programmierung auf doktrinorientierte Weise gelöst werden können.

DI unterscheidet klar zwischen Parametern und Kontext. Parameter werden an die Funktion übergeben und der Kontext wird vom DI-Container über den Konstruktor übergeben. Die Abhängigkeiten werden im Konstruktor deutlich angezeigt. DI ist im Kern sehr einfach. Als zusätzlichen Bonus hat Code mit DI-Containern den Vorteil, "normaler" Code zu sein. Jeder Programmierer, der die Sprache beherrscht, kann sie natürlich lesen (abgesehen davon bin ich aus diesem Grund nicht sehr gern mit Feldinjektion in Java beschäftigt).

Was den Kontext von den Parametern trennt, ist eigentlich ziemlich vage. Selbst mit der im obigen Beispiel angegebenen "Konfiguration" gibt es natürlich Situationen, in denen es einfacher zu verstehen ist, ob sie als Parameter und nicht als Kontext behandelt wird (beispielsweise ist es eine gute Idee, eine JSON-formatierte Konfigurationszeichenfolge in eine "Konfigurations" -Instanz zu analysieren. Es wäre natürlicher, eine Funktion zu sein. Dieses Argument ist jedoch weiterhin gültig, da ** in der Funktion ** deutlich zeigt, was sich im Kontext befindet und was der Parameter für die Person ist, die den Code liest, wodurch die kognitive Belastung verringert wird. Weil es dazu beiträgt.

Leader-Monaden haben den entscheidenden Vorteil, Kontext und Parameter zu trennen. Gleiches gilt für DI-Container. Und DI ist viel einfacher zu starten als Monad.

Scala implizit

Natürlich hat Scala Scalaz, und wenn Sie Scalaz nicht verwenden, werden Sie als Java-Programmierer betrachtet und verspottet (!). Interessanterweise hat Scala auch eine Funktion namens "implizit", die eine kontextbezogene Abstraktion ermöglicht. In Video, das zuvor für das Codebeispiel verwendet wurde werde ich den von Professor Odelsky vorgestellten Code zitieren.

type Configured[T] = implicit Config => T

def readPerson: Configured[Option[Person]] =
  attempt(
    Some(Person(readName, readAge))
  ).onError(None)

Mit "implizit" können Sie Kontextparameter ohne lästige Handschriftargumente übergeben. Übrigens ist es dank des Compilers und des Curry effizienter, als einen Abschluss zu machen. Und wie bei DI müssen Sie keinen "seltsamen" Code wie Monad schreiben.

Ich bin kein Scala-Programmierer und kann nicht sagen, ob "implizit" funktioniert. Es gibt jedoch einige Bedenken.

Was ich hier jedoch sagen wollte, ist, dass es verschiedene Techniken gibt, um mit dem Kontext umzugehen. Monade ist nicht der einzig richtige Weg.

Java EE

JSR 365 und CDI sind für die DI-Funktion in Java EE verantwortlich. Zuvor war der Eindruck, den ich bei der Arbeit mit Java EE hatte, der schlimmste. Das Schlimmste bedeutet besser als EJB.

Kürzlich habe ich jedoch die Spezifikationen diagonal gelesen, und der Eindruck hat sich erheblich geändert. Ich schämte mich zu sagen, dass ich CDI kritisierte, ohne die Details zu kennen, nur mit dem Wissen, das ich online suchte. Vielleicht war es allgemein bekannt für alle, aber lassen Sie mich so tun, als wäre ich vergessen.

Der typische CDI-Code scheint am Anfang dieses Beitrags angezeigt worden zu sein. Beschreiben Sie die abhängigen Module als Parameter des Konstruktors mit @ Inject.

class BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  @Inject
  BillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    ...
  }
}

(Dieses Beispiel stammt aus Guice, nicht genau aus JSR 365 ... Guices Wiki lernt DI Ich denke, es ist eine sehr nützliche Ressource, also schauen Sie bitte)

Ich denke, DI-Frameworks in jeder Sprache werden fast gleich aussehen. Beim Testen können Sie die Implementierung von "CreditCardProcessor" so ändern, dass Sie nicht für die eigentliche Karte bezahlen müssen.

Übrigens kann in Java EE die Klasse "HttpServletRequest" das Ziel von DI sein. Ich konnte nicht anders, als mich die ganze Zeit darüber zu wundern. IoC sollte ein Mechanismus zum Entkoppeln von Implementierungen sein, indem auf abstrakte Module zurückgegriffen wird. Die Anfrage besteht jedoch nur aus Daten. Ist es nicht natürlich, es als Argument wie funktionale Programmierung zu übergeben? In der Tat [Als Argument in einem Raw-Servlet empfangen](https://docs.oracle.com/javaee/7/api/javax/ servlet / http / HttpServlet.html # doGet-javax.servlet.http.HttpServletRequest-javax.servlet.http.HttpServletResponse-) Das stimmt. Ist es zum Beispiel nicht seltsam, eine Anfrage in ein Singleton-Modul einfügen zu können? Auf der Implementierungsebene wird die tatsächlich injizierte Instanz über einen Proxy abgerufen. Der Proxy tauscht die entsprechenden Instanzen für jeden Kontext aus, sodass Sie in Module mit einem breiteren Bereich einfügen können. Aber ist es notwendig, es so zu machen?

CDI ist also mehr als nur ein DI-Framework. Es fungiert nicht nur als IoC-Container, sondern bietet auch den Aspekt, Funktionen zum aktiven Abstrahieren und Behandeln des Kontexts bereitzustellen.

Der folgende Code ist ein Auszug aus einer Beispielspezifikation.

@SessionScoped @Model
public class Login implements Serializable {
    @Inject Credentials credentials;
    @Inject @Users EntityManager userDatabase;

    private CriteriaQuery<User> query;
    private Parameter<String> usernameParam;
    private Parameter<String> passwordParam;

    private User user;

    @Inject
    void initQuery( @Users EntityManagerFactory emf ) {
        CriteriaBuilder cb = emf.getCriteriaBuilder();
        usernameParam = cb.parameter( String.class );
        passwordParam = cb.parameter( String.class );
        query = cb.createQuery( User.class );
        Root<User> u = query.from( User.class );
        query.select( u );
        query.where(
                cb.equal( u.get( User_.username ), usernameParam ),
                cb.equal( u.get( User_.password ), passwordParam )
        );
    }

    public void login() {
        List<User> results = userDatabase.createQuery( query )
                                         .setParameter( usernameParam, credentials.getUsername() )
                                         .setParameter( passwordParam, credentials.getPassword() )
                                         .getResultList();
        if ( !results.isEmpty() ) {
            user = results.get( 0 );
        }
    }

    public void logout() {
        user = null;
    }

    public boolean isLoggedIn() {
        return user != null;
    }

    @Produces @LoggedIn User getCurrentUser() {
        if ( user == null ) {
            throw new NotLoggedInException();
        } else {
            return user;
        }
    }
}

Es ist sicherlich voll von Java-ähnlicher Redundanz, und ich gebe zu, dass es viele unangenehme Teile gibt (wie zum Beispiel initQuery). Aber wenn Sie es aus einem kontextuellen Blickwinkel betrachten, funktioniert diese Klasse wirklich perfekt.

@ SessionScoped gibt an, dass die Instanz für jede HTTP-Sitzung instanziiert wird.

<f:view>
    <h:form>
        <h:panelGrid columns="2" rendered="#{!login.loggedIn}">
            <h:outputLabel for="username">Username:</h:outputLabel>
            <h:inputText id="username" value="#{credentials.username}"/>
            <h:outputLabel for="password">Password:</h:outputLabel>
            <h:inputText id="password" value="#{credentials.password}"/>
        </h:panelGrid>
        <h:commandButton value="Login" action="#{login.login}" rendered="#{!login.loggedIn}"/>
        <h:commandButton value="Logout" action="#{login.logout}" rendered="#{login.loggedIn}"/>
    </h:form>
</f:view>

Das Tolle ist, dass Klassen, die diese Klasse verwenden, sich überhaupt nicht um den Sitzungsstatus oder die Verbindung zur Datenbank kümmern müssen. Sie können einfach die Klasse "Anmelden" "@ Injizieren" und auf den Kontext zugreifen, ohne zu wissen, was sich hinter den Kulissen abspielt. Sie müssen sich nicht einmal bewusst sein, dass die Anmeldeklasse der Sitzungsbereich ist. Dies ist ein Trick, der in der funktionalen Programmierung nicht nachgeahmt werden kann (die Login-Klasse selbst ist ein statusbehaftetes Objekt, das den Kontext des Anmeldevorgangs verkörpert). Diese Klasse ist ein veränderliches Objekt, aber der Anmeldestatus unterscheidet sich in erster Linie wesentlich. Die Änderungen werden direkt im Code modelliert. Weitere leistungsstarke Funktionen wie Lifecycle-Hooks, Ereignisse, Interceptors usw. werden bereitgestellt, die hier nicht im Detail behandelt werden.

Aus diesem Grund ist Javas DI nicht nur DI, sondern heißt ** Context and ** Dependency Injection.

Tatsächlich bin ich immer noch ziemlich skeptisch, wenn ich gefragt werde, ob ich ein Programm in Java EE schreiben möchte. Der Nachteil der Redundanz ist schwerwiegend, und ich denke nicht, dass die hier erwähnte Kontextsteuerung in * allen * Fällen funktioniert. Eine variable Zustandskontrolle kann tragisch sein, wenn Sie einen Fehler machen. Es ist jedoch klar, dass die Person, die die JSR 365-Spezifikation erstellt hat, das Problem des Kontexts ernst nahm und es sehr sorgfältig entwarf (obwohl es sehr unhöflich ist, dies zu schreiben). Ich bedauere, dass es nicht gut ist, zu kritisieren, ohne etwas zu wissen.

Zusammenfassung

Das Problem bei der Ablehnung von Abhängigkeiten besteht darin, dass Kontextprobleme ignoriert werden. Monaden sind zu schwierig, um den Kontext zu abstrahieren. DI ist nicht perfekt, aber es ermöglicht Ihnen, relativ einfach mit dem Kontext zu arbeiten. Sie müssen sich nicht minderwertig fühlen, nur weil Sie Java verwenden und Monad nicht kennen.

Wenn Sie diesen Beitrag lesen und Jakarta EE übernehmen und Ihr Projekt brennt, bin ich nicht verantwortlich.

Recommended Posts

Warum Dependency Injection nicht so schlecht ist
Dolch2 - Android-Abhängigkeitsinjektion
DI: Was ist Abhängigkeitsinjektion?
Fassen Sie Ruby und Dependency Injection zusammen