Wie hilft funktionaler Stil beim Verspotten von Abhängigkeiten?

10

Aus dem Interview mit Kent Beck in einer aktuellen Ausgabe des Java Magazine:

Binstock: Lassen Sie uns über Microservices sprechen. Es scheint mir, dass Test-First auf Microservices in dem Sinne kompliziert werden würde, dass einige Dienste, um zu funktionieren, das Vorhandensein einer ganzen Reihe anderer Dienste benötigen. Sind Sie einverstanden?

Beck: Es scheint dasselbe zu sein, wenn man eine große Klasse oder viele kleine Klassen hat.

Binstock: Richtig, außer ich denke, hier müssen Sie eine Menge Mocks verwenden, um ein System einrichten zu können, mit dem Sie einen bestimmten Dienst testen können.

Beck: Ich bin anderer Meinung. Wenn es sich um einen imperativen Stil handelt, müssen Sie viele Mocks verwenden. In einem funktionalen Stil, in dem externe Abhängigkeiten hoch oben in der Aufrufkette gesammelt werden, halte ich das nicht für notwendig. Ich denke, Sie können durch Unit-Tests viel Berichterstattung erhalten.

Was meint er? Wie kann der funktionale Stil Sie davon befreien, sich über externe Abhängigkeiten lustig zu machen?

Dan
quelle
1
Wenn sie speziell über Java diskutieren, vermute ich, dass ein Großteil dieser Diskussion umstritten ist. Java hat nicht wirklich die Unterstützung, die es braucht, um sich für die Art der beschriebenen funktionalen Programmierung zu eignen. Sicher können Sie Utility-Klassen oder Java 8 Lambdas verwenden, um dies zu simulieren, aber ... blecch.
Robert Harvey
1
Siehe auch: softwareengineering.stackexchange.com/q/5757
Robert Harvey

Antworten:

8

Eine reine Funktion ist eine, die:

  1. Gibt immer das gleiche Ergebnis mit den gleichen Argumenten
  2. Hat keine beobachtbaren Nebenwirkungen (zB Zustandsänderungen)

Angenommen, wir schreiben Code für die Benutzeranmeldung, in dem wir überprüfen möchten, ob der angegebene Benutzername und das angegebene Kennwort korrekt sind, und verhindern, dass sich der Benutzer anmeldet, wenn zu viele Fehlversuche auftreten. In einem imperativen Stil könnte unser Code folgendermaßen aussehen:

bool UserLogin(string username, string password)
{
    var user = _database.FindUser(username);
    if (user == null)
    {
        return false;
    }
    if (user.FailedAttempts > 3)
    {
        return false;
    }
    // Password hashing omitted for brevity
    if (user.Password != password)
    {
        _database.RecordFailedLoginAttempt(username);
    }
    return true;
}

Es ist ziemlich klar, dass dies keine reine Funktion ist:

  1. Diese Funktion wird nicht immer das gleiche Ergebnis für ein gegebene usernameund passwordKombination als das Ergebnis hängt auch von dem Benutzerdatensatz in der Datenbank gespeichert.
  2. Die Funktion kann den Status der Datenbank ändern, dh sie hat Nebenwirkungen.

Beachten Sie auch, dass wir zum Testen dieser Funktion zwei Datenbankaufrufe verspotten müssen, FindUserund RecordFailedLoginAttempt.

Wenn wir diesen Code in einen funktionaleren Stil umgestalten würden, könnten wir am Ende etwas Ähnliches erhalten:

bool UserLogin(string username, string password)
{
    var user = _database.FindUser(username);
    var result = UserLoginPure(user, password);
    if (result == Result.FailedAttempt)
    {
        _database.RecordFailedLoginAttempt(username);
    }
    return result == Result.Success;
}

Result UserLoginPure(User user, string pasword)
{
    if (user == null)
    {
        return Result.UserNotFound;
    }
    if (user.FailedAttempts > 3)
    {
        return Result.LoginAttemptsExceeded;
    }
    if (user.Password != password)
    {
        return Result.FailedAttempt;        
    }
    return Result.Success;
}

Beachten Sie, dass die UserLoginFunktion zwar immer noch nicht rein ist, die UserLoginPureFunktion jetzt jedoch eine reine Funktion ist. Infolgedessen kann die Kernlogik der Benutzerauthentifizierung auf Einheit getestet werden, ohne dass externe Abhängigkeiten verspottet werden müssen. Dies liegt daran, dass die Interaktion mit der Datenbank weiter oben im Aufrufstapel erfolgt.

Justin
quelle
Ist Ihre Interpretation: imperativer Stil = zustandsbehaftete Mikrodienste und funktionaler Stil = zustandslose Mikrodienste ?
k3b
@ k3b Art, bis auf das bisschen über Mikrodienste. Ganz einfach beinhaltet der imperative Stil die Manipulation des Zustands, während der funktionale Stil reine Funktionen ohne Zustandsmanipulation verwendet.
Justin
1
@Justin: Ich würde sagen, dass der funktionale Stil reine Funktionen klar vom Code mit Nebenwirkungen trennt, wie Sie es in Ihrem Beispiel getan haben. Mit anderen Worten, Funktionscode kann immer noch Nebenwirkungen haben.
Giorgio
Der funktionale Ansatz sollte ein Paar mit einem Ergebnis und einem Benutzer zurückgeben, da bei einem fehlgeschlagenen Versuch Result.FailedAttempt das Ergebnis eines neuen Benutzers mit denselben Daten wie das Original ist, außer dass ein weiterer fehlgeschlagener Versuch vorliegt und eine reine Funktion dies tut Induzieren Sie Nebenwirkungen auf den Benutzer, die als Parameter angegeben werden.
Steigende Dunkelheit
Korrektur für den letzten Teil meines vorherigen Kommentars: "und eine reine Funktion verursacht KEINE Nebenwirkungen auf den Benutzer, die als Parameter angegeben werden."
Steigende Dunkelheit