Prinzip der einzelnen Verantwortung, auch bekannt als Prinzip der einzelnen Verantwortung,
das auch als Prinzip der einzelnen Veränderlichkeit bekannt ist – ein sehr rutschiger und schwer verständlicher Begriff und eine so nervenaufreibende Frage im Vorstellungsgespräch für Programmierer.
Meine erste ernsthafte Begegnung mit diesem Prinzip fand zu Beginn des ersten Semesters statt, als wir, jung und unerfahren, in den Wald gebracht wurden, um aus Larven von Studenten – echte Studenten – zu machen.
Im Wald wurden wir in Gruppen von 8-9 Personen eingeteilt und es fand ein Wettbewerb statt – welche Gruppe schneller eine Flasche Wodka leert, unter der Bedingung, dass die erste Person aus der Gruppe den Wodka ins Glas einschenkt, die zweite ihn trinkt und die dritte nachschlägt. Die Person, die ihre Aufgabe abgeschlossen hat, reiht sich am Ende der Schlange der Gruppe ein.
Ein Fall, bei dem die Größe der Schlange ein Vielfaches von drei war, stellte eine gute Umsetzung des SRP dar.
Definition 1. Einzige Verantwortung.
Offizielle Definition des Prinzips der einzelnen Verantwortung (SRP): Jeder Objekt hat seine eigene Verantwortung und einen Grund für seine Existenz, und diese Verantwortung ist die einzige.
Betrachten wir das Objekt „Wodkatrinker“ (Tippler).
Um das SRP-Prinzip einzuhalten, teilen wir die Aufgaben auf drei Personen auf:
- Einer schenkt ein (PourOperation)
- Einer trinkt (DrinkUpOperation)
- Einer beißt (TakeBiteOperation)
Jeder der Teilnehmer des Prozesses ist für eine Komponente des Prozesses verantwortlich, das heißt, hat eine atomare Verantwortung – zu trinken, zu gießen oder nachzuschlagen.
Der Wodkatrinker ist in diesem Fall das Interface für diese Operationen:
class Tippler {
//...
void Act() {
_pourOperation.Do() // gießen
_drinkUpOperation.Do() // trinken
_takeBiteOperation.Do() // nachschlagen
}
}
Warum?
Der Programmierer schreibt Code für den Affen, und der Affe ist unaufmerksam, dumm und ständig in Eile. Er kann etwa 3 bis 7 Begriffe zu einem Zeitpunkt halten und verstehen.
Im Fall des Wodkatrinkers sind es diese drei Begriffe. Wenn wir jedoch gesamten Code in einem Stück schreiben, tauchen Hände, Gläser, Schlägereien und endlose Diskussionen über Politik auf. Und all dies wird im Körper einer Methode sein. Ich bin mir sicher, dass Sie solchen Code in Ihrer Praxis gesehen haben. Kein sehr humanes Experiment für die Psyche.
Andererseits ist der Mensch-Affe darauf ausgelegt, Objekte der realen Welt in seinem Kopf zu modellieren. In seiner Vorstellung kann er sie zusammenstoßen, neue Objekte aus ihnen zusammensetzen und ebenso auseinandernehmen. Stellen Sie sich ein altes Automodell vor. Sie können sich vorstellen, die Tür zu öffnen, die Türverkleidung abzuschrauben und die Mechanismen der Fensterheber zu sehen, in denen sich Zahnräder befinden. Aber Sie können nicht alle Komponenten des Autos gleichzeitig in einem 'Listing' sehen. Zumindest kann das der 'Mensch-Affe' nicht.
Deshalb zerlegen Programmierer komplexe Mechanismen in eine Menge weniger komplexer und funktionierender Elemente. Allerdings kann man unterschiedlich zerlegen: In vielen alten Autos führt der Luftkanal in die Tür, während in modernen Autos ein Elektronikfehler im Schloss den Motorstart verhindert, was bei der Reparatur Schwierigkeiten verursacht.
Also, SRP ist ein Prinzip, das erklärt, WIE man zerlegt, also wo man die Trennlinie ziehen sollte..
Es besagt, dass man nach dem Prinzip der 'Verantwortungsaufteilung' zerlegen sollte, das heißt nach den Aufgaben der jeweiligen Objekte.
Kehren wir zu dem Trinker und den Vorteilen zurück, die der Mensch-Affe beim Zerlegen erhält:
- Der Code ist auf jeder Ebene extrem klar.
- Der Code kann von mehreren Programmierern gleichzeitig geschrieben werden (jeder schreibt ein separates Element).
- Automatisierte Tests werden einfacher - je einfacher das Element, desto leichter ist es, es zu testen.
- Es entsteht eine Kompositionalität des Codes - man kann ersetzen. DrinkUpOperation durch eine Operation, bei der der Trinker Flüssigkeit unter den Tisch gießt. Oder man kann die Einschenken-Operation durch eine Operation ersetzen, bei der man Wein und Wasser oder Wodka und Bier mischt. Je nach den Anforderungen des Geschäfts können Sie alles tun, ohne den Codes des Verfahrens zu berühren. Tippler.Act.
- Aus diesen Operationen können Sie eine Verschwendung zusammenstellen (indem Sie nur TakeBitOperation), einen Alkoholiker (indem Sie nur DrinkUpOperation direkt aus der Flasche) und viele andere Geschäftsanforderungen zufriedenstellen.
(Oh, das scheint bereits das OCP-Prinzip zu sein, und ich habe die Verantwortung dieses Beitrags verletzt.)
Und natürlich die Nachteile:
- Es müssen mehr Typen geschaffen werden.
- Der Trinker wird das erste Mal ein paar Stunden später trinken, als er könnte.
Definition 2. Einzigartige Änderbarkeit.
Lassen Sie das, meine Herren! Die Klasse des Trinkers hat ebenfalls eine eindeutige Verantwortung – sie trinkt! Und überhaupt, das Wort „Verantwortung“ ist ein äußerst vages Konzept. Manche sind verantwortlich für das Schicksal der Menschheit, andere sind verantwortlich dafür, umgekehrte Pinguine am Pol aufzuheben.
Betrachten wir zwei Implementierungen des Trinkers. Die erste, die oben erwähnt wurde, umfasst drei Klassen – einschenken, trinken und snacken.
Die zweite, geschrieben nach der Methodologie „Vorwärts und nur vorwärts“, enthält die gesamte Logik in der Methode Act:
//Не тратьте время на изучение этого класса. Лучше съешьте печеньку
с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 dieser Klassen erscheinen aus der Sicht eines Außenstehenden völlig identisch und erfüllen die einzige Verantwortung „trinken“.
Peinlich!
Dann gehen wir ins Internet und erfahren eine andere Definition des SRP – Prinzip der einzigen Änderbarkeit (Single Changeability Principle).
SCP besagt, dass „Ein Modul hat einen und nur einen Grund zur Änderung“. Das heißt „Verantwortung ist der Grund für eine Änderung“.
(Es scheint, dass die Leute, die die ursprüngliche Definition erfunden haben, von den telepathischen Fähigkeiten des Menschenaffen überzeugt waren)
Jetzt passt alles an seinen Platz. Man kann die Einschenk-, Trink- und Snackprozesse separat ändern, und im eigentlichen Trinker können wir nur die Reihenfolge und den Inhalt der Vorgänge ändern, zum Beispiel indem wir den Snack vor dem Trinken anordnen oder das Vorlesen eines Trinkspruchs hinzufügen.
Im Ansatz „Vorwärts und nur vorwärts“ kann alles, was geändert werden kann, nur in der Methode geändert werden Act. Das kann lesbar und effizient sein, wenn es nur wenig Logik gibt und sie sich selten ändert, aber oft endet das in schrecklichen Methoden mit 500 Zeilen in jeder, mit mehr if-Anweisungen als erforderlich für den Beitritt Russlands zur NATO.
Definition 3. Lokalisierung von Änderungen.
Trinker verstehen oft nicht, warum sie in einer fremden Wohnung aufgewacht sind oder wo ihr Handy ist. Es ist an der Zeit, detailliertes Logging hinzuzufügen.
Lassen Sie uns das Logging mit dem Einschenkprozess beginnen:
class PourOperation: IOperation{
PourOperation(ILogger log
/*....*/){
/*...*/}
//...
void Do(){
_log.Log($"Vor dem Einschenken mit {_hand} und {_bottle}");
//Einschank-Logik ...
_log.Log($"Nach dem Einschenken mit {_hand} und {_bottle}");
}
}indem wir es in PourOperation, wir haben klug in Bezug auf Verantwortung und Kapselung gehandelt, aber nun haben wir ein Problem mit dem Prinzip der Veränderlichkeit. Neben der Operation, die sich ändern kann, wird auch das Logging selbst variabel. Wir müssen trennen und einen speziellen Logger für die Gießoperation 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{
//... Geschäftslogik
_log.LogAfter(_hand, _bottle);
}
catch(exception e){
_log.OnError(_hand, _bottle, e)
}
}
}Ein genauer Leser wird bemerken, dass LogAfter, LogBefore und OnError auch einzeln variieren können, und analog zu den vorherigen Aktionen drei Klassen erstellen wird: PourLoggerBefore, PourLoggerAfter und PourErrorLogger.
Und wenn wir uns erinnern, dass es drei Arten von Trinkoperationen gibt, erhalten wir neun Logging-Klassen. Insgesamt besteht unser Trink-Framework aus 14 (!!!) Klassen.
Hyperbel? Kaum! Ein Mensch-Affenkind mit einer dekompositionellen Granate wird den „Gießer“ in Karaffe, Glas, Einspeisefunktionen, Wasserdienste, physikalische Molekülkollisionsmodelle zerlegen und das nächste Quartal damit verbringen, Abhängigkeiten ohne globale Variablen zu entwirren. Und glauben Sie mir – er wird nicht aufhören.
Genau in diesem Moment kommen viele zu dem Schluss, dass SRP Märchen aus rosa Königreichen sind, und sie gehen, um sich anderen Blindheiten hinzugeben...
… und erfahren nie von der Existenz der dritten Definition von SRP:
„Das Prinzip der Einzelverantwortung besagt, dass ähnliche Änderungsobjekte an einem Ort aufbewahrt werden sollten“oder „Was zusammen geändert wird, sollte an einem Ort aufbewahrt werden.”
Das heißt, wenn wir das Logging der Operation ändern, müssen wir dies an einem Ort tun.
Das ist ein sehr wichtiger Punkt – denn während alle früheren Erklärungen zu SRP besagten, dass man Typen zerlegen sollte, bis sie zerlegbar sind, was eine „obere Grenze“ auf die Größe des Objekts auferlegte, sprechen wir jetzt auch von einer „unteren Grenze“. Mit anderen Worten, SRP erfordert nicht nur, dass Dinge so lange zerlegt werden, wie sie zerlegbar sind, sondern auch, dass man es nicht übertreibt – „nicht eng verbundene Dinge zu sehr zerlegen“. Das ist der große Kampf zwischen Ockhams Rasiermesser und dem Menschen-Affen!
Jetzt sollte es für den Trinkenden einfacher werden. Neben der Tatsache, dass wir den Logger IPourLogger nicht in drei Klassen zerlegen müssen, können wir auch alle Logger in 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 ein vierter Operationstyp hinzugefügt wird, ist das Logging dafür bereits vorbereitet. Und der Code der Operationen selbst ist sauber und frei von infrastrukturellem Lärm.
Insgesamt haben wir 5 Klassen zur Lösung der Trinkaufgabe:
- Abfülloperation
- Trinkoperation
- Abbeißoperation
- Logger
- Spirituosen-Fassade
Jede dieser Klassen ist streng für eine Funktionalität verantwortlich und hat einen einzigen Grund zur Änderung. Alle ähnlichen Änderungsregeln liegen nah beieinander.
Beispiel aus dem echten Leben
Einmal haben wir einen Service zur automatischen Registrierung von b2b-Kunden geschrieben. Und es entstand eine GOD-Methode mit 200 Zeilen ähnlichen Inhalts:
- Geh zu 1C und lege ein Konto an
- Gehe mit diesem Konto zum Zahlungsmodul und lege es dort an
- Überprüfe, ob ein Konto mit diesem Konto im Hauptsystem nicht existiert Server
- Erstelle ein neues Konto
- Füge das Ergebnis der Registrierung im Zahlungsmodul und die 1C-Nummer dem Registrierungsservice hinzu
- Füge diese Tabelle mit Informationen über das Konto hinzu
- Erstelle eine Punktnummer für diesen Kunden im Punkteservice. Übergebe die 1C-Nummer an diesen Service.
In dieser Liste gab es noch etwa 10 weitere Geschäftsoperationen mit schrecklicher Verknüpfung. Das Kontoobjekt war fast überall erforderlich. Die Punkt-ID und der Kundenname waren in der Hälfte der Aufrufe nötig.
Nach einer einstündigen Refaktorisierung konnten wir den Infrastrukturcode und einige Nuancen der Kontobearbeitung in separate Methoden/Klassen auslagern. Die God-Methode wurde leichter, aber es blieben 100 Zeilen Code, die sich einfach nicht auflösen wollten.
Erst nach einigen Tagen wurde uns klar, dass das Wesen dieser "leichter gewordenen" Methode der Geschäftsalgorithmus ist. Und dass die ursprüngliche Beschreibung des Lastenhefts ziemlich kompliziert war. Und dass der Versuch, diese Methode aufzubrechen, eine Verletzung des SRP darstellen würde, nicht umgekehrt.
Formalisierung.
Es ist an der Zeit, unseren Spirituosenverkäufer ruhen zu lassen. Wisch dir die Tränen weg – wir werden eines Tages zu ihm zurückkehren. Aber jetzt formalisiere ich das Wissen aus diesem Artikel.
Formalisierung 1. Definition von SRP
- Trennt die Elemente so, dass jedes von ihnen für etwas Einziges verantwortlich ist.
- Verantwortung wird als „Grund für eine Änderung“ entschlüsselt. Das heißt, jedes Element hat nur einen Grund für eine Änderung, in Bezug auf die Geschäftslogik.
- Potenzielle Änderungen der Geschäftslogik sollten lokalisiert werden. Änderbare synchronisierte Elemente sollten nah beieinander liegen.
Formalismus 2. Notwendige Kriterien zur Selbstprüfung.
Mir sind keine ausreichenden Kriterien zur Erfüllung des SRP begegnet. Aber es gibt notwendige Bedingungen:
1) Stellen Sie sich die Frage – was macht diese Klasse/Methode/Modul/Dienst? Sie sollten mit einer einfachen Definition darauf antworten. (Danke) )
Erläuterungen
Manchmal ist es jedoch sehr schwierig, eine einfache Definition zu finden.
2) Die Behebung eines bestimmten Fehlers oder die Hinzufügung einer neuen Funktion betrifft die minimale Anzahl von Dateien/Klassen. Ideal wäre — eine.
Erläuterungen
Da die Verantwortung (für die Funktion oder den Fehler) in einer einzigen Datei/Klasse gekapselt ist, wissen Sie genau, wo Sie suchen und was Sie ändern müssen. Zum Beispiel: Die Funktion zur Änderung der Ausgabe der Logoperations würde nur den Logger erfordern. Es ist nicht erforderlich, den gesamten restlichen Code zu durchforsten.
Ein anderes Beispiel — die Hinzufügung eines neuen UI-Elements, das den vorherigen ähnelt. Wenn Sie dadurch 10 verschiedene Entitäten und 15 verschiedene Konverter hinzufügen müssen, sieht es so aus, als hätten Sie "übertrieben".
3) Wenn mehrere Entwickler an verschiedenen Funktionen Ihres Projekts arbeiten, ist die Wahrscheinlichkeit eines Merge-Konflikts, also die Wahrscheinlichkeit, dass dieselbe Datei/Klasse von mehreren Entwicklern gleichzeitig geändert wird, minimal.
Erläuterungen
Wenn Sie bei der Hinzufügung einer neuen Operation „Wodka unter den Tisch gießen“ den Logger, die Trink- und Gießoperation berühren müssen, sieht es so aus, als wären die Verantwortlichkeiten nicht korrekt getrennt. Sicherlich ist das nicht immer möglich, aber man sollte versuchen, diesen Wert zu minimieren.
4) Bei einer klärenden Frage zur Geschäftslogik (vom Entwickler oder Manager) greifen Sie strikt auf eine einzige Klasse/Datei zu und erhalten Informationen nur von dort.
Erläuterungen
Funktionen, Regeln oder Algorithmen sind kompakt, jeweils an einem Ort geschrieben und nicht über Flags im gesamten Code verstreut.
5) Die Benennung ist klar.
Erläuterungen
Unsere Klasse oder Methode ist für etwas Bestimmtes verantwortlich, und diese Verantwortung spiegelt sich in ihrem Namen wider.
AllManagersManagerService — wahrscheinlich eine God-Klasse.
LocalPayment — wahrscheinlich nicht.
Formalismus 3. Entwicklungsansatz „Ockhams erstes Prinzip“.
Zu Beginn des Designs weiß die Mensch-Äffchen nicht und spürt nicht alle Feinheiten der zu lösenden Aufgabe und kann einen Fehler machen. Man kann auf verschiedene Arten falsch liegen:
- Zu große Objekte erstellen, indem verschiedene Verantwortlichkeiten zusammengefügt werden
- Zerlegen, indem eine einzige Verantwortung in viele verschiedene Typen aufgeteilt wird
- Verantwortungsgrenzen falsch definieren
Es ist wichtig, sich an das Prinzip zu erinnern: "Es ist besser, in die andere Richtung zu irren" oder "wenn du dir nicht sicher bist, zerlege nicht". Wenn zum Beispiel deine Klasse zwei Verantwortungen vereint, ist sie immer noch verständlich und kann mit minimalen Änderungen im Client-Code in zwei Teile zerlegt werden. Aus Glassplittern ein Glas zusammenzusetzen, ist in der Regel schwieriger, da der Kontext auf mehrere Dateien verstreut ist und die notwendigen Abhängigkeiten im Client-Code fehlen.
Es ist an der Zeit, zum Ende zu kommen
Der Anwendungsbereich des SRP ist nicht auf OOP und SOLID beschränkt. Es ist anwendbar auf Methoden, Funktionen, Klassen, Module, Mikrodienste und Dienste. Es findet Anwendung sowohl bei "Fickfack-fickfack-und-in-Produktion" als auch bei "Raketenwissenschaft"-Entwicklung und macht überall die Welt ein kleines Stück besser. Wenn man darüber nachdenkt, ist es kaum mehr als ein grundlegendes Prinzip der gesamten Ingenieurwissenschaften. Maschinenbau, Kontrollsysteme, und überhaupt alle komplexen Systeme werden aus Komponenten gebaut, und "Unterteilung" beraubt Konstrukteure der Flexibilität, "Überteilung" der Effizienz, und falsche Grenzen der Vernunft und des seelischen Friedens.
SRP ist nicht von der Natur erfunden worden und ist kein Teil der exakten Wissenschaft. Es ergibt sich aus unseren biologischen und psychologischen Einschränkungen. Es ist lediglich ein Weg, komplexe Systeme mit dem menschlichen Gehirn zu steuern und zu entwickeln. Es zeigt uns, wie man ein System dekonstruiert. Die ursprüngliche Formulierung erforderte einen gewissen Grad an Telepathie, aber ich hoffe, dieser Artikel hat den Nebel ein wenig durchbrochen.
Quelle: habr.com
