Prinzip der Einzelverantwortung. Nicht so einfach, wie es scheint

Prinzip der Einzelverantwortung. Nicht so einfach, wie es scheint Prinzip der Einzelverantwortung, auch Prinzip der Einzelverantwortung genannt,
auch bekannt als das Prinzip der gleichmäßigen Variabilität – ein äußerst schwierig zu verstehender Typ und eine so nervöse Frage bei einem Programmiererinterview.

Meine erste ernsthafte Bekanntschaft mit diesem Prinzip fand zu Beginn meines ersten Jahres statt, als die Jungen und Grünen in den Wald gebracht wurden, um aus Larven echte Schüler zu machen.

Im Wald wurden wir in Gruppen zu je 8-9 Personen eingeteilt und veranstalteten einen Wettbewerb – welche Gruppe würde am schnellsten eine Flasche Wodka trinken, vorausgesetzt, der erste aus der Gruppe gießt Wodka in ein Glas, der zweite trinkt ihn, und der dritte isst einen Snack. Die Einheit, die ihre Operation abgeschlossen hat, rückt an das Ende der Warteschlange der Gruppe.

Der Fall, dass die Warteschlangengröße ein Vielfaches von drei war, war eine gute Implementierung von SRP.

Definition 1. Einzelverantwortung.

Die offizielle Definition des Single-Responsibility-Prinzips (SRP) besagt, dass jedes Unternehmen seine eigene Verantwortung und seinen eigenen Existenzgrund hat und dass es nur eine Verantwortung hat.

Betrachten Sie das Objekt „Trinker“ (Tipper).
Um das SRP-Prinzip umzusetzen, werden wir die Verantwortlichkeiten in drei Teile aufteilen:

  • Man gießt (PourOperation)
  • Einer trinkt (DrinkUpOperation)
  • Man isst einen Snack (TakeBiteOperation)

Jeder Prozessbeteiligte ist für eine Komponente des Prozesses verantwortlich, hat also eine atomare Verantwortung – zu trinken, einzuschenken oder zu naschen.

Das Trinkloch wiederum ist eine Fassade für diese Operationen:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

Prinzip der Einzelverantwortung. Nicht so einfach, wie es scheint

Warum?

Der menschliche Programmierer schreibt Code für den Affenmenschen, und der Affenmensch ist unaufmerksam, dumm und immer in Eile. Er kann ca. 3 – 7 Begriffe gleichzeitig beherrschen und verstehen.
Im Falle eines Trunkenboldes gibt es drei dieser Begriffe. Wenn wir den Code jedoch auf einem Blatt schreiben, enthält er Hände, Brillen, Kämpfe und endlose Auseinandersetzungen über Politik. Und das alles wird im Hauptteil einer Methode enthalten sein. Ich bin sicher, dass Sie solchen Code in Ihrer Praxis gesehen haben. Nicht der humanste Test für die Psyche.

Andererseits ist der Affenmensch so konzipiert, dass er reale Objekte in seinem Kopf simuliert. In seiner Fantasie kann er sie zusammenschieben, daraus neue Objekte zusammensetzen und auf die gleiche Weise auch wieder zerlegen. Stellen Sie sich ein altes Modellauto vor. In Ihrer Fantasie können Sie die Tür öffnen, die Türverkleidung abschrauben und dort die Fensterhebermechanismen sehen, in deren Inneren sich Zahnräder befinden. Sie können jedoch nicht alle Komponenten der Maschine gleichzeitig in einer „Auflistung“ sehen. Zumindest der „Affenmann“ kann das nicht.

Daher zerlegen menschliche Programmierer komplexe Mechanismen in eine Reihe weniger komplexer und funktionierender Elemente. Der Abbau kann jedoch auf unterschiedliche Weise erfolgen: Bei vielen alten Autos geht der Luftkanal in die Tür, und bei modernen Autos verhindert ein Defekt in der Schlosselektronik das Starten des Motors, was bei Reparaturen zum Problem werden kann.

Und so, SRP ist ein Prinzip, das erklärt, WIE man zerlegt, das heißt, wo die Trennlinie zu ziehen ist.

Er sagt, es sei notwendig, nach dem Prinzip der Aufteilung der „Verantwortung“, also nach den Aufgaben bestimmter Objekte, zu zerlegen.

Prinzip der Einzelverantwortung. Nicht so einfach, wie es scheint

Kehren wir zum Trinken und den Vorteilen zurück, die der Affenmann bei der Zersetzung erhält:

  • Der Code ist auf jeder Ebene äußerst klar geworden
  • Der Code kann von mehreren Programmierern gleichzeitig geschrieben werden (jeder schreibt ein separates Element).
  • Automatisierte Tests werden vereinfacht – je einfacher das Element, desto einfacher ist es zu testen
  • Die Zusammensetzung des Codes erscheint - Sie können ihn ersetzen DrinkUpOperation zu einer Operation, bei der ein Betrunkener Flüssigkeit unter den Tisch schüttet. Oder ersetzen Sie den Einschenkvorgang durch einen Vorgang, bei dem Sie Wein und Wasser oder Wodka und Bier mischen. Abhängig von den Geschäftsanforderungen können Sie alles tun, ohne den Methodencode zu berühren Tippler.Act.
  • Mit diesen Vorgängen können Sie den Vielfraß falten (nur mit TakeBitOperation), Alkoholiker (nur unter Verwendung von DrinkUpOperation direkt aus der Flasche) und erfüllen viele weitere Geschäftsanforderungen.

(Oh, es scheint, dass dies bereits ein OCP-Prinzip ist und ich gegen die Verantwortung dieses Beitrags verstoßen habe.)

Und natürlich die Nachteile:

  • Wir müssen mehr Typen erstellen.
  • Ein Betrunkener trinkt zum ersten Mal ein paar Stunden später, als er es sonst getan hätte.

Definition 2. Einheitliche Variabilität.

Erlauben Sie mir, meine Herren! Der Trinkkurs hat auch eine einzige Verantwortung: Er trinkt! Und im Allgemeinen ist das Wort „Verantwortung“ ein äußerst vager Begriff. Jemand ist für das Schicksal der Menschheit verantwortlich, und jemand ist für die Aufzucht der Pinguine verantwortlich, die an der Stange umgestürzt sind.

Betrachten wir zwei Implementierungen des Trinkers. Die erste, oben erwähnte, enthält drei Klassen: Einschenken, Trinken und Snacken.

Die zweite ist nach der „Forward and Only Forward“-Methodik geschrieben und enthält die gesamte Logik der Methode Handlung:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
   //...
   void Act(){
        // наливаем
    if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
        throw new OverdrunkException();

    // выпиваем
    if(!_hand.TryDrink(from: _glass,  size: _glass.Capacity))
        throw new OverdrunkException();

    //Закусываем
    for(int i = 0; i< 3; i++){
        var food = _foodStore.TakeOrDefault();
        if(food==null)
            throw new FoodIsOverException();

        _hand.TryEat(food);
    }
   }
}

Beide Klassen sehen aus der Sicht eines externen Beobachters völlig gleich aus und tragen die gleiche Verantwortung für das „Trinken“.

Verwirrung!

Dann gehen wir online und finden eine weitere Definition von SRP heraus – das Single Changeability Principle.

SCP gibt an, dass „Ein Modul hat einen und nur einen Grund, sich zu ändern". Das heißt: „Verantwortung ist ein Grund für Veränderung.“

(Es scheint, dass die Leute, die sich die ursprüngliche Definition ausgedacht haben, von den telepathischen Fähigkeiten des Affenmenschen überzeugt waren.)

Jetzt passt alles zusammen. Getrennt davon können wir die Eingieß-, Trink- und Snackvorgänge ändern, im Trinker selbst können wir jedoch nur die Reihenfolge und Zusammensetzung der Vorgänge ändern, indem wir beispielsweise den Snack vor dem Trinken verschieben oder das Vorlesen eines Toasts hinzufügen.

Beim „Forward and Only Forward“-Ansatz wird alles, was geändert werden kann, nur in der Methode geändert Handlung. Dies kann lesbar und effektiv sein, wenn es wenig Logik gibt und es sich selten ändert, aber oft endet es in schrecklichen Methoden mit jeweils 500 Zeilen und mehr Wenn-Anweisungen, als für den Beitritt Russlands zur NATO erforderlich sind.

Definition 3. Lokalisierung von Änderungen.

Trinker verstehen oft nicht, warum sie in der Wohnung eines anderen aufgewacht sind oder wo sich ihr Mobiltelefon befindet. Es ist Zeit, eine detaillierte Protokollierung hinzuzufügen.

Beginnen wir mit der Protokollierung des Gießvorgangs:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_bottle}");
    }
}

Indem man es einkapselt PourOperation, wir haben unter dem Gesichtspunkt der Verantwortung und Kapselung klug gehandelt, aber jetzt sind wir mit dem Prinzip der Variabilität verwechselt. Neben dem Vorgang selbst, der sich ändern kann, wird auch die Protokollierung selbst veränderbar. Sie müssen einen speziellen Logger für den Gießvorgang trennen und erstellen:

interface IPourLogger{
    void LogBefore(IHand, IBottle){}
    void LogAfter(IHand, IBottle){}
    void OnError(IHand, IBottle, Exception){}
}

class PourOperation: IOperation{
    PourOperation(IPourLogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.LogBefore(_hand, _bottle);
        try{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

Das wird dem aufmerksamen Leser auffallen LogAfter, LogBefore и OnError kann auch individuell geändert werden und erzeugt analog zu den vorherigen Schritten drei Klassen: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

Und wenn man bedenkt, dass es für einen Trinker drei Operationen gibt, erhalten wir neun Protokollierungsklassen. Somit besteht der gesamte Trinkzirkel aus 14 (!!!) Klassen.

Hyperbel? Kaum! Ein Affenmensch mit einer Zersetzungsgranate wird den „Ausgießer“ in eine Karaffe, ein Glas, Ausgießer, einen Wasserversorgungsdienst und ein physikalisches Modell der Kollision von Molekülen aufteilen und im nächsten Quartal versuchen, die Abhängigkeiten außerhalb zu entwirren globale Variablen. Und glauben Sie mir, er wird nicht aufhören.

An diesem Punkt kommen viele zu dem Schluss, dass SRP Märchen aus rosa Königreichen sind, und gehen weg, um Nudeln zu spielen ...

... ohne jemals etwas über die Existenz einer dritten Definition von Srp zu erfahren:

„Das Prinzip der Einzelverantwortung besagt dies Dinge, die einer Veränderung ähneln, sollten an einem Ort gespeichert werden". oder "Was sich gemeinsam ändert, sollte an einem Ort aufbewahrt werden"

Das heißt, wenn wir die Protokollierung eines Vorgangs ändern, müssen wir sie an einer Stelle ändern.

Dies ist ein sehr wichtiger Punkt, da in allen oben aufgeführten SRP-Erläuterungen darauf hingewiesen wurde, dass es notwendig sei, die Typen während des Zerkleinerns zu zerkleinern, d Wir sprechen bereits von einer „Untergrenze“. Mit anderen Worten, SRP erfordert nicht nur „Zerkleinern beim Zerkleinern“, sondern auch, es nicht zu übertreiben – „Ineinandergreifende Dinge nicht zerdrücken“. Dies ist der große Kampf zwischen Occams Rasiermesser und dem Affenmenschen!

Prinzip der Einzelverantwortung. Nicht so einfach, wie es scheint

Jetzt sollte sich der Trinker besser fühlen. Abgesehen davon, dass es nicht notwendig ist, den IPourLogger-Logger in drei Klassen aufzuteilen, können wir auch alle Logger zu einem Typ zusammenfassen:

class OperationLogger{
    public OperationLogger(string operationName){/*..*/}
    public void LogBefore(object[] args){/*...*/}       
    public void LogAfter(object[] args){/*..*/}
    public void LogError(object[] args, exception e){/*..*/}
}

Und wenn wir einen vierten Operationstyp hinzufügen, ist die Protokollierung dafür bereits fertig. Und der Code der Abläufe selbst ist sauber und frei von Infrastrukturlärm.

Als Ergebnis haben wir 5 Kurse zur Lösung des Alkoholproblems:

  • Gießvorgang
  • Trinkbetrieb
  • Einklemmvorgang
  • Logger
  • Trinkerfassade

Jeder von ihnen ist ausschließlich für eine Funktionalität verantwortlich und hat einen Grund für die Änderung. Alle änderungsähnlichen Regeln liegen in der Nähe vor.

Beispiel aus dem wirklichen Leben

Wir haben einmal einen Dienst zur automatischen Registrierung eines B2B-Kunden geschrieben. Und für 200 Zeilen mit ähnlichem Inhalt erschien eine GOD-Methode:

  • Gehen Sie zu 1C und erstellen Sie ein Konto
  • Gehen Sie mit diesem Konto zum Zahlungsmodul und erstellen Sie es dort
  • Stellen Sie sicher, dass auf dem Hauptserver kein Konto mit einem solchen Konto erstellt wurde
  • Ein neues Konto erstellen
  • Fügen Sie die Registrierungsergebnisse im Zahlungsmodul und die 1c-Nummer zum Registrierungsergebnisdienst hinzu
  • Fügen Sie dieser Tabelle Kontoinformationen hinzu
  • Erstellen Sie im Punktdienst eine Punktnummer für diesen Kunden. Geben Sie Ihre 1c-Kontonummer an diesen Dienst weiter.

Und auf dieser Liste standen etwa zehn weitere Geschäftsbetriebe mit schrecklicher Konnektivität. Fast jeder brauchte das Kontoobjekt. Bei der Hälfte der Anrufe waren die Punkt-ID und der Name des Kunden erforderlich.

Nach einer Stunde Refactoring konnten wir den Infrastrukturcode und einige Nuancen der Arbeit mit einem Konto in separate Methoden/Klassen aufteilen. Die God-Methode machte es einfacher, aber es waren noch 100 Codezeilen übrig, die einfach nicht entwirrt werden wollten.

Erst nach wenigen Tagen wurde klar, dass die Essenz dieser „leichten“ Methode ein Geschäftsalgorithmus ist. Und dass die ursprüngliche Beschreibung der technischen Spezifikationen recht komplex war. Und es ist der Versuch, diese Methode in Stücke zu zerlegen, der gegen die SRP verstößt, und nicht umgekehrt.

Formalismus.

Es ist Zeit, unseren Betrunkenen in Ruhe zu lassen. Trockne deine Tränen – wir werden auf jeden Fall eines Tages darauf zurückkommen. Lassen Sie uns nun das Wissen aus diesem Artikel formalisieren.

Formalismus 1. Definition von SRP

  1. Trennen Sie die Elemente so, dass jedes von ihnen für eine Sache verantwortlich ist.
  2. Verantwortung steht für „Grund zur Veränderung“. Das heißt, jedes Element hat aus Sicht der Geschäftslogik nur einen Grund für eine Änderung.
  3. Mögliche Änderungen an der Geschäftslogik. müssen lokalisiert werden. Elemente, die sich synchron ändern, müssen in der Nähe sein.

Formalismus 2. Notwendige Selbsttestkriterien.

Ich habe keine ausreichenden Kriterien zur Erfüllung des SRP gesehen. Aber es gibt notwendige Bedingungen:

1) Fragen Sie sich, was diese Klasse/Methode/Modul/Dienst bewirkt. Sie müssen es mit einer einfachen Definition beantworten. ( Danke Brightori )

Erklärungen

Manchmal ist es jedoch sehr schwierig, eine einfache Definition zu finden

2) Das Beheben eines Fehlers oder das Hinzufügen einer neuen Funktion betrifft eine Mindestanzahl von Dateien/Klassen. Idealerweise - eins.

Erklärungen

Da die Verantwortung (für eine Funktion oder einen Fehler) in einer Datei/Klasse gekapselt ist, wissen Sie genau, wo Sie suchen und was Sie bearbeiten müssen. Beispiel: Die Funktion zum Ändern der Ausgabe von Protokollierungsvorgängen erfordert nur die Änderung des Loggers. Es ist nicht erforderlich, den Rest des Codes durchzugehen.

Ein weiteres Beispiel ist das Hinzufügen eines neuen UI-Steuerelements, ähnlich den vorherigen. Wenn Sie dadurch gezwungen sind, 10 verschiedene Entitäten und 15 verschiedene Konverter hinzuzufügen, sieht es so aus, als würden Sie es übertreiben.

3) Wenn mehrere Entwickler an verschiedenen Funktionen Ihres Projekts arbeiten, ist die Wahrscheinlichkeit eines Zusammenführungskonflikts, d. h. die Wahrscheinlichkeit, dass dieselbe Datei/Klasse gleichzeitig von mehreren Entwicklern geändert wird, minimal.

Erklärungen

Wenn Sie beim Hinzufügen einer neuen Operation „Wodka unter den Tisch gießen“ den Logger, den Vorgang des Trinkens und Ausschenkens, beeinflussen müssen, dann sieht es so aus, als ob die Verantwortlichkeiten schief verteilt sind. Natürlich ist das nicht immer möglich, aber wir sollten versuchen, diese Zahl zu reduzieren.

4) Wenn Ihnen eine klärende Frage zur Geschäftslogik gestellt wird (von einem Entwickler oder Manager), gehen Sie strikt in eine Klasse/Datei und erhalten Informationen nur von dort.

Erklärungen

Funktionen, Regeln oder Algorithmen werden kompakt geschrieben, jeweils an einem Ort, und nicht mit Flags über den gesamten Coderaum verstreut.

5) Die Benennung ist klar.

Erklärungen

Unsere Klasse oder Methode ist für eine Sache verantwortlich, und die Verantwortung spiegelt sich in ihrem Namen wider

AllManagersManagerService – höchstwahrscheinlich eine Gottklasse
LocalPayment – ​​wahrscheinlich nicht

Formalismus 3. Occam-First-Entwicklungsmethodik.

Zu Beginn des Entwurfs kennt und spürt der Affenmensch nicht alle Feinheiten des zu lösenden Problems und kann einen Fehler machen. Sie können auf unterschiedliche Weise Fehler machen:

  • Machen Sie Objekte zu groß, indem Sie verschiedene Verantwortlichkeiten zusammenführen
  • Neuausrichtung durch Aufteilung einer einzelnen Verantwortung in viele verschiedene Arten
  • Definieren Sie die Grenzen der Verantwortung falsch

Es ist wichtig, sich an die Regel zu erinnern: „Es ist besser, einen großen Fehler zu machen“ oder „Wenn Sie sich nicht sicher sind, teilen Sie es nicht auf.“ Wenn Ihre Klasse beispielsweise zwei Verantwortlichkeiten enthält, ist sie dennoch verständlich und kann mit minimalen Änderungen am Client-Code in zwei Teile geteilt werden. Das Zusammensetzen eines Glases aus Glassplittern ist in der Regel schwieriger, da der Kontext über mehrere Dateien verteilt ist und im Client-Code keine notwendigen Abhängigkeiten vorhanden sind.

Es ist Zeit, Schluss zu machen

Der Umfang von SRP ist nicht auf OOP und SOLID beschränkt. Es gilt für Methoden, Funktionen, Klassen, Module, Microservices und Services. Es gilt sowohl für die „figax-figax-and-prod“- als auch für die „raketenwissenschaftliche“ Entwicklung und macht die Welt überall ein wenig besser. Wenn Sie darüber nachdenken, ist dies fast das Grundprinzip aller Ingenieurwissenschaften. Maschinenbau, Steuerungssysteme und tatsächlich alle komplexen Systeme bestehen aus Komponenten, und „Unterfragmentierung“ beraubt Designer der Flexibilität, „Überfragmentierung“ beraubt Designer der Effizienz und falsche Grenzen berauben sie der Vernunft und des Seelenfriedens.

Prinzip der Einzelverantwortung. Nicht so einfach, wie es scheint

SRP wurde nicht von der Natur erfunden und ist nicht Teil der exakten Wissenschaft. Es durchbricht unsere biologischen und psychologischen Grenzen. Es ist lediglich eine Möglichkeit, komplexe Systeme mithilfe des Gehirns des Affenmenschen zu steuern und zu entwickeln. Er sagt uns, wie man ein System zerlegt. Die ursprüngliche Formulierung erforderte einiges an Telepathie, aber ich hoffe, dass dieser Artikel einiges von der Nebelwand beseitigt.

Source: habr.com

Kommentar hinzufügen