Prinzip der Einzelverantwortung. Nicht so einfach wie es sich anhört

Bild Prinzip der Einzelverantwortung, er ist das Prinzip der Einzelverantwortung,
Er ist das Prinzip der einheitlichen Variabilität - ein extrem rutschiger Typ, den man verstehen muss, und eine so nervöse Frage beim Interview mit einem Programmierer.


Die erste ernsthafte Bekanntschaft mit diesem Prinzip fand für mich zu Beginn des ersten Jahres statt, als wir aus den Jungen und Grünen in den Wald gebracht wurden, um echte Schüler aus den Larven zu machen.


Im Wald wurden wir in Gruppen von jeweils 8-9 Personen aufgeteilt und organisierten einen Wettbewerb - welche Gruppe wird schneller eine Flasche Wodka trinken, vorausgesetzt, die erste Person aus der Gruppe gießt Wodka in ein Glas, die zweite trinkt und die dritte nimmt einen Bissen. Nach Abschluss des Betriebs steht das Gerät am Ende der Gruppenwarteschlange.


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


Definition 1. Einzelverantwortung.


Die offizielle Definition des Prinzips der Einzelverantwortung (SRP) legt nahe, dass jedes Objekt seine eigene Verantwortung und seinen eigenen Existenzgrund hat, und diese Verantwortung hat nur einen.


Betrachten Sie das Tippler-Objekt.
Um das SRP-Prinzip zu erfüllen, teilen wir die Verantwortlichkeiten in drei Bereiche:


  • Man gießt ( PourOperation )
  • One Drinks ( DrinkUpOperation )
  • Ein Snack ( TakeBiteOperation )

Jeder der Teilnehmer des Prozesses ist für eine Komponente des Prozesses verantwortlich, dh er hat eine atomare Verantwortung - zu trinken, zu gießen oder einen Bissen zu haben.


Der Alkohol wiederum ist die Fassade für diese Operationen:


lass Tippler { //... void Act(){ _pourOperation.Do() //  _drinkUpOperation.Do() //  _takeBiteOperation.Do() //  } } 

Bild

Warum?


Der menschliche Programmierer schreibt den Code für den Affenmann, und der Affenmann ist unaufmerksam, dumm und hat es immer irgendwo eilig. Er kann ungefähr 3 bis 7 Begriffe gleichzeitig halten und verstehen.
Im Falle von Alkohol sind diese Begriffe drei. Wenn wir den Code jedoch mit einem Blatt schreiben, erscheinen darin Hände, Brillen, Massaker und endlose Debatten über Politik. Und all dies wird im Körper einer Methode sein. Ich bin sicher, Sie haben einen solchen Code in Ihrer Praxis gesehen. Nicht der humanste Test für die Psyche.


Auf der anderen Seite ist der Affenmann eingesperrt, weil er reale Objekte in seinem Kopf modelliert hat. In seiner Vorstellung kann er sie zusammenschieben, neue Objekte von ihnen sammeln und sie auf die gleiche Weise zerlegen. Stellen Sie sich ein altes Automodell vor. Sie können die Tür in Ihrer Fantasie öffnen, die Türverkleidung abschrauben und dort die Fensterhebermechanismen sehen, in denen sich Zahnräder befinden. Sie können jedoch nicht alle Komponenten der Maschine gleichzeitig in einer "Liste" sehen. Zumindest der "Affenmann" kann das nicht.


Daher zerlegen menschliche Programmierer komplexe Mechanismen in eine Reihe weniger komplexer und funktionierender Elemente. Die Zersetzung kann jedoch auf verschiedene Arten erfolgen: In vielen alten Autos - der Kanal geht aus der Tür und in modernen - verhindert das Versagen der Schlosselektronik das Starten des Motors, der während der Reparatur liefert.


SRP ist also ein Prinzip, das erklärt, wie man zerlegt, dh wo die Trennlinie gezogen wird .


Er sagt, dass die Zersetzung auf dem Prinzip der Trennung von "Verantwortung" beruhen sollte, dh auf den Aufgaben verschiedener Objekte.


Bild

Kehren wir zum Alkohol und den Vorteilen zurück, die eine Affenperson beim Zerlegen hat:


  • Der Code ist auf jeder Ebene extrem klar geworden.
  • Mehrere Programmierer können gleichzeitig Code schreiben (jeder schreibt ein separates Element).
  • Das automatisierte Testen wird vereinfacht - je einfacher das Element ist, desto einfacher ist es zu testen
  • Aus diesen drei Vorgängen können Sie in Zukunft den Vielfraß (nur mit TakeBitOperation ) und den Alkoholiker (nur mit DrinkUpOperation direkt aus der Flasche) addieren und viele andere Geschäftsanforderungen erfüllen.

Und natürlich die Nachteile:


  • Muss mehr Typen erstellen.
  • Ein Trinker wird ein paar Stunden später zum ersten Mal trinken, als er konnte

Definition 2. Einheitliche Variabilität.


Erlauben Sie meine Herren! Die Trinkklasse erfüllt auch eine einzige Verantwortung - sie trinkt! Und im Allgemeinen ist das Wort "Verantwortung" ein äußerst vages Konzept. Jemand ist für das Schicksal der Menschheit verantwortlich, und jemand ist dafür verantwortlich, die an der Stange umgekippten Pinguine aufzuziehen.


Betrachten Sie zwei Bingo-Implementierungen. Die erste, oben erwähnte, enthält drei Klassen - gießen, trinken und beißen.


Die zweite Methode basiert auf der Forward- und Only Forward-Methode und enthält die gesamte Logik der Act- Methode:


 //      .    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 Sicht eines externen Beobachters genau gleich aus und erfüllen die alleinige Verantwortung des „Trinkens“.


Verlegenheit!


Dann surfen wir im Internet und finden eine andere Definition von SRP heraus - das Prinzip der einheitlichen Variabilität.


Diese Definition besagt: " Das Modul hat nur einen Grund für Änderungen ." Das heißt: "Verantwortung ist Anlass für Veränderungen."


Jetzt passt alles zusammen. Separat können Sie die Gieß-, Trink- und Beißverfahren ändern, und im Alkohol selbst können wir nur die Reihenfolge und Zusammensetzung der Vorgänge ändern, z. B. den Snack vor dem Trinken bewegen oder einen Toastwert hinzufügen.


Beim Forward and Only Forward-Ansatz wird alles, was geändert werden kann, nur bei der Act- Methode geändert. Es kann lesbar und effektiv sein, wenn es wenig Logik gibt und es sich selten ändert, aber es endet oft mit schrecklichen Methoden von jeweils 500 Zeilen mit mehr Wenn-Zahlen, 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 Handy befindet. Es ist Zeit, eine detaillierte Protokollierung hinzuzufügen.


Beginnen wir mit der Protokollierung mit dem Gießvorgang:


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

Wir haben es in PourOperation zusammengefasst und in Bezug auf Verantwortung und Verkapselung klug gehandelt, aber jetzt, mit dem Prinzip der Variabilität, sind wir jetzt verlegen. Zusätzlich zu der Operation selbst, die sich ändern kann, wird die Protokollierung selbst variabel. Wir müssen uns trennen und einen speziellen Logger für den Gießvorgang herstellen:


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

Ein akribischer Leser wird feststellen, dass LogAfter , LogBefore und OnError auch einzeln geändert werden können. In Analogie zu den vorherigen Schritten werden drei Klassen erstellt: PourLoggerBefore , PourLoggerAfter und PourErrorLogger .


Und wenn wir uns daran erinnern, dass es drei Operationen für einen Binge gibt - wir erhalten neun Protokollierungsklassen. Infolgedessen besteht der gesamte Alkohol aus 14 (!!!) Klassen.


Übertreibung? Kaum! Ein Affenmensch mit einer Zersetzungsgranate wird den „Ausgießer“ in einen Dekanter, ein Glas, Gießbetreiber, einen Wasserversorgungsdienst, ein physikalisches Modell einer Kollision von Molekülen zerdrücken und im nächsten Quartal versuchen, die Abhängigkeiten ohne globale Variablen zu enträtseln. Und glauben Sie mir - er wird nicht aufhören.


An diesem Punkt kommen viele zu dem Schluss, dass SRPs Geschichten aus den rosa Königreichen sind, und lassen die Nudeln verdrehen ...


... nie über die Existenz der dritten Definition von Srp Bescheid wissen:


" Dinge, die Veränderungen ähneln, sollten an einem Ort aufbewahrt werden ." oder " Was sich gemeinsam ändert, muss an einem Ort aufbewahrt werden "


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


Dies ist ein sehr wichtiger Punkt - da alle oben genannten SRP-Erklärungen besagten, dass Typen geteilt werden sollten, während sie geteilt werden, dh die Größe des Objekts einer „obersten Beschränkung“ unterworfen sind, und jetzt von einer „unteren Grenze“ sprechen. . Mit anderen Worten, SRP erfordert nicht nur "Quetschen während des Quetschens", sondern auch keine Übertreibung - "Quetschen Sie keine verknüpften Dinge". Komplizieren Sie nicht unnötig. Dies ist die große Schlacht von Occams Rasiermesser mit dem Affenmann!


Bild

Jetzt sollte der Alkohol leichter sein. Wir können den IPourLogger-Logger nicht nur in drei Klassen aufteilen, sondern 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 uns die vierte Art von Operation hinzugefügt wird, ist die Protokollierung dafür bereit. Und der Code der Operationen selbst ist sauber und frei von Infrastrukturlärm.


Als Ergebnis haben wir 5 Klassen zur Lösung des Trinkproblems:


  • Gießvorgang
  • Getränkebetrieb
  • Staubetrieb
  • Logger
  • Fassade der Booler

Jeder von ihnen ist streng für eine Funktionalität verantwortlich, hat einen Grund für Änderungen. Alle Regeln, die Änderungen ähneln, liegen in der Nähe.


Beispiele aus dem wirklichen Leben


Serialisierung und Deserialisierung

Im Rahmen der Entwicklung des Datenübertragungsprotokolls ist es erforderlich, eine Art "Benutzer" zu einer Zeichenfolge zu serialisieren und zu deserialisieren.


 User{ String Name; Int Age; } 

Sie könnten denken, dass Serialisierung und Deserialisierung in getrennten Klassen durchgeführt werden müssen:


 UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} } 

Da jeder von ihnen seine eigene Verantwortung und einen Grund für Veränderungen hat.


Sie haben jedoch einen gemeinsamen Grund für Änderungen: "Ändern des Formats der Datenserialisierung".
Und wenn Sie dieses Format ändern, ändern sich Serialisierung und Deserialisierung immer.


Nach dem Prinzip der Lokalisierung von Änderungen müssen wir sie zu einer Klasse zusammenfassen:


 UserSerializer{ String deserialize(User){...} User serialize(String){...} } 

Dies erspart uns unnötige Komplexität und die Notwendigkeit, sich daran zu erinnern, dass Sie sich bei jedem Wechsel des Serializers an den Deserializer erinnern müssen.


Zählen und speichern

Sie müssen den Jahresumsatz des Unternehmens berechnen und in der Datei C: \ results.txt speichern.


Wir lösen dies schnell mit einer Methode:


 void SaveGain(Company company){ //     //   } 

Bereits aus der Definition der Aufgabe geht hervor, dass es zwei Unteraufgaben gibt - "Umsatz berechnen" und "Umsatz sparen". Jeder von ihnen hat einen Grund für Änderungen - "eine Änderung der Berechnungsmethode" und "eine Änderung des Speicherformats". Diese Änderungen überschneiden sich nicht. Außerdem können wir die Frage „Was macht die SaveGain-Methode?“ Nicht einsilbig beantworten. Diese AND- Methode berechnet den Umsatz UND speichert die Ergebnisse.


Daher müssen Sie diese Methode in zwei Teile teilen:


 Gain CalcGain(Company company){..} void SaveGain(Gain gain){..} 

Vorteile:


  • kann separat getestet werden CalcGain
  • Es ist einfacher, Fehler zu lokalisieren und Änderungen vorzunehmen
  • Die Lesbarkeit des Codes wurde erhöht
  • Das Fehlerrisiko bei jeder der Methoden wird aufgrund ihrer Vereinfachung verringert

Anspruchsvolle Geschäftslogik

Einmal haben wir einen Service für die automatische Registrierung eines B2B-Clients geschrieben. Und es gab eine GOTT-Methode mit 200 Zeilen ähnlichen Inhalts:


  • Gehen Sie zu 1C und erstellen Sie ein Konto
  • Gehen Sie mit diesem Konto zum Zahlungsmodul und rufen Sie es dort auf
  • Stellen Sie sicher, dass auf dem Hauptserver kein Konto mit einem solchen Konto erstellt wurde
  • Erstellen Sie ein neues Konto
  • Das Registrierungsergebnis im Zahlungsmodul und die Nummer 1c werden dem Registrierungsergebnisdienst hinzugefügt
  • Fügen Sie dieser Tabelle Kontoinformationen hinzu
  • Erstellen Sie im Punktedienst eine Punktnummer für diesen Kunden. Geben Sie diesem Dienstkonto die Nummer 1s.

Es gab ungefähr 10 weitere Geschäftsvorgänge mit schrecklicher Verbundenheit auf dieser Liste. Das Kontoobjekt wurde von fast allen benötigt. Punkt-ID und Client-Name wurden in der Hälfte der Anrufe benötigt.


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


Nur wenige Tage später kam das Verständnis, dass das Wesentliche dieser "erleichterten" Methode der Geschäftsalgorithmus ist. Und dass die anfängliche Beschreibung von TK ziemlich kompliziert war. Und es ist ein Versuch, diese Methode in Teile zu zerlegen, die eine Verletzung von SRP darstellen, und nicht umgekehrt.


Formalismus.


Es ist Zeit, unseren Alkohol in Ruhe zu lassen. Wischen Sie die Tränen weg - wir werden auf jeden Fall irgendwie darauf zurückkommen. Jetzt formalisieren wir das Wissen aus diesem Artikel.


Formalismus 1. Definition von SRP


  1. Trennen Sie die Elemente so, dass jedes für eine Sache verantwortlich ist.
  2. Verantwortung steht für „Grund zur Veränderung“. Das heißt, jedes Element hat nur einen Grund für Änderungen in Bezug auf die Geschäftslogik.
  3. Mögliche Änderungen der Geschäftslogik. muss lokalisiert werden. Elemente, die zusammen veränderlich sind, müssen in der Nähe sein.

Formalismus 2. Notwendige Kriterien für die Selbstprüfung.


Ich habe nicht genügend Kriterien für die Implementierung von SRP erfüllt. Aber es gibt notwendige Bedingungen:


1) Stellen Sie sich eine Frage - was macht diese Klasse / Methode / Modul / Dienst. Sie müssen es mit einer einfachen Definition beantworten. (danke an 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 wirkt sich auf die Mindestanzahl von Dateien / Klassen aus. Im Idealfall eine.


Erklärungen

Da die Verantwortung (für ein Feature oder einen Fehler) in einer einzelnen Datei / Klasse zusammengefasst ist, wissen Sie genau, wo Sie suchen und was Sie bearbeiten müssen. Beispiel: Für die Funktion zum Ändern der Ausgabe der Operationsprotokollierung muss nur der Logger geändert werden. Das Ausführen des restlichen Codes ist nicht erforderlich.


Ein weiteres Beispiel ist das Hinzufügen eines neuen UI-Steuerelements ähnlich den vorherigen. Wenn Sie dadurch gezwungen werden, 10 verschiedene Entitäten und 15 verschiedene Konverter hinzuzufügen, haben Sie anscheinend "kaputt".


3) Wenn mehrere Entwickler an unterschiedlichen Funktionen Ihres Projekts arbeiten, ist die Wahrscheinlichkeit eines Zusammenführungskonflikts, dh die Wahrscheinlichkeit, dass mehrere Entwickler dieselbe Datei / Klasse gleichzeitig ändern, minimal.


Erklärungen

Wenn Sie beim Hinzufügen einer neuen Operation "Wodka unter den Tisch gießen" den Logger berühren müssen, die Operation des Trinkens und Gießens, dann sieht es so aus, als ob die Verantwortlichkeiten schief sind. Dies ist natürlich nicht immer möglich, aber Sie müssen versuchen, diese Zahl zu reduzieren.


4) Wenn Sie eine Frage zur Geschäftslogik (von einem Entwickler oder Manager) klären, klettern Sie streng in eine Klasse / Datei und erhalten nur von dort Informationen.


Erklärungen

Features, Regeln oder Algorithmen werden kompakt an einer Stelle geschrieben und nicht durch Flags im gesamten Codebereich verteilt.


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 Gottklasse
LocalPayment - wahrscheinlich nicht


Formalismus 3. Die Entwicklungsmethode von Occam-first.


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


  • Machen Sie Objekte zu groß, indem Sie unterschiedliche Verantwortlichkeiten festlegen
  • Teilen Sie eine einzelne Verantwortung in viele verschiedene Typen auf
  • Falsch definierte Verantwortungsgrenzen

Es ist wichtig, sich an die Regel zu erinnern: "Es ist besser, einen großen Fehler zu machen" oder "nicht sicher - nicht trennen". Wenn Ihre Klasse beispielsweise zwei Verantwortlichkeiten sammelt, ist dies immer noch verständlich und kann mit einer minimalen Änderung des Client-Codes in zwei Teile aufgeteilt werden. Das Sammeln eines Glases aus Glasfragmenten ist aufgrund des über mehrere Dateien verteilten Kontexts und des Fehlens notwendiger Abhängigkeiten im Clientcode normalerweise schwieriger.


Es ist Zeit abzurunden


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 Entwicklung von „figax-figax-and-in-prod“ als auch für „rocket-sainz“, wodurch die Welt überall ein wenig besser wird. Wenn Sie darüber nachdenken, ist dies fast das Grundprinzip aller Technik. Maschinenbau, Steuerungssysteme und in der Tat alle komplexen Systeme bestehen aus Komponenten, und die „unvollständige Fragmentierung“ beraubt die Konstrukteure der Flexibilität, der „Fragmentierung“ - der Effizienz und der falschen Grenzen - der Vernunft und des Seelenfriedens.


Bild

SRP ist nicht von Natur aus erfunden und nicht Teil der exakten Wissenschaft. Es kriecht aus unseren biologischen und psychologischen Grenzen heraus. Dies ist nur eine Möglichkeit, komplexe Systeme mithilfe des Gehirns eines menschlichen Affen zu steuern und zu entwickeln. Er sagt uns, wie wir das System zerlegen sollen. Der ursprüngliche Wortlaut erforderte einiges an Telepathie, aber ich hoffe, dieser Artikel hat die Nebelwand leicht zerstreut.

Source: https://habr.com/ru/post/de454290/


All Articles