Principe van één verantwoordelijkheid. Niet zo eenvoudig als het lijkt

Principe van één verantwoordelijkheid. Niet zo eenvoudig als het lijkt Het beginsel van enkele verantwoordelijkheid, ook wel het beginsel van enkele verantwoordelijkheid genoemd,
ook wel het principe van uniforme variabiliteit genoemd - een extreem gladde man om te begrijpen en zo'n nerveuze vraag tijdens een interview met een programmeur.

Mijn eerste serieuze kennismaking met dit principe vond plaats aan het begin van het eerste jaar, toen de jonge en groene kinderen naar het bos werden gebracht om van larven studenten te maken - echte studenten.

In het bos werden we verdeeld in groepen van elk 8-9 personen en hadden we een wedstrijd: welke groep zou het snelst een fles wodka drinken, op voorwaarde dat de eerste persoon uit de groep wodka in een glas giet, de tweede drinkt het op, en de derde heeft een tussendoortje. De eenheid die zijn operatie heeft voltooid, gaat naar het einde van de wachtrij van de groep.

Het geval waarin de wachtrijgrootte een veelvoud van drie was, was een goede implementatie van SRP.

Definitie 1. Eén verantwoordelijkheid.

De officiële definitie van het Single Responsibility Principle (SRP) stelt dat elke entiteit zijn eigen verantwoordelijkheid en bestaansreden heeft, en slechts één verantwoordelijkheid.

Beschouw het object "Drinker" (Tippler).
Om het SRP-principe te implementeren, verdelen we de verantwoordelijkheden in drie:

  • Men giet (Gietwerking)
  • Eén drankje (DrinkUp-bediening)
  • Men heeft een tussendoortje (TakeBiteOperation)

Elk van de deelnemers aan het proces is verantwoordelijk voor één onderdeel van het proces, dat wil zeggen, heeft één atomaire verantwoordelijkheid: drinken, schenken of snacken.

Het drinkgat is op zijn beurt een façade voor deze operaties:

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

Principe van één verantwoordelijkheid. Niet zo eenvoudig als het lijkt

Waarom?

De menselijke programmeur schrijft code voor de aap-mens, en de aap-mens is onoplettend, dom en heeft altijd haast. Hij kan ongeveer 3 tot 7 termen tegelijk vasthouden en begrijpen.
In het geval van een dronkaard zijn er drie van deze termen. Als we de code echter met één blad schrijven, bevat deze handen, brillen, gevechten en eindeloze discussies over politiek. En dit alles zal in de hoofdtekst van één methode zijn. Ik weet zeker dat je dergelijke code in je praktijk hebt gezien. Niet de meest humane test voor de psyche.

Aan de andere kant is de aapmens ontworpen om objecten uit de echte wereld in zijn hoofd te simuleren. In zijn verbeelding kan hij ze samendrukken, er nieuwe voorwerpen uit samenstellen en ze op dezelfde manier weer uit elkaar halen. Stel je een oude modelauto voor. In je verbeelding kun je de deur openen, de deurbekleding losschroeven en daar de raamliftmechanismen zien, waarbinnen versnellingen zitten. Maar je kunt niet alle componenten van de machine tegelijkertijd in één "lijst" zien. De “aapman” kan dat in ieder geval niet.

Daarom ontleden menselijke programmeurs complexe mechanismen in een reeks minder complexe en werkende elementen. Het kan echter op verschillende manieren worden afgebroken: bij veel oude auto's gaat het luchtkanaal de deur in, en bij moderne auto's verhindert een storing in de slotelektronica dat de motor start, wat een probleem is tijdens reparaties.

nu, SRP is een principe dat uitlegt HOE je moet ontbinden, dat wil zeggen waar je de scheidslijn moet trekken.

Hij zegt dat het noodzakelijk is om te ontbinden volgens het principe van verdeling van 'verantwoordelijkheid', dat wil zeggen volgens de taken van bepaalde objecten.

Principe van één verantwoordelijkheid. Niet zo eenvoudig als het lijkt

Laten we terugkeren naar drinken en de voordelen die de aapmens krijgt tijdens de ontbinding:

  • De code is op elk niveau uiterst duidelijk geworden
  • De code kan door meerdere programmeurs tegelijk worden geschreven (elk schrijft een afzonderlijk element)
  • Geautomatiseerd testen is vereenvoudigd: hoe eenvoudiger het element, hoe gemakkelijker het is om te testen
  • De compositoriteit van de code verschijnt - u kunt deze vervangen DrinkUp-bediening tot een operatie waarbij een dronkaard vloeistof onder de tafel giet. Of vervang de schenkhandeling door een handeling waarbij u wijn en water of wodka en bier mengt. Afhankelijk van de zakelijke vereisten kunt u alles doen zonder de methodecode aan te raken Tippler.Act.
  • Van deze bewerkingen kun je de veelvraat vouwen (alleen met behulp van TakeBitOperatie), Alcoholisch (alleen gebruiken DrinkUp-bediening rechtstreeks uit de fles) en voldoen aan vele andere zakelijke vereisten.

(Oh, het lijkt erop dat dit al een OCP-principe is, en ik heb de verantwoordelijkheid van dit bericht geschonden)

En natuurlijk de nadelen:

  • We zullen meer typen moeten creëren.
  • Een dronkaard drinkt voor het eerst een paar uur later dan hij anders zou hebben gedaan.

Definitie 2. Uniforme variabiliteit.

Sta mij toe, heren! De drinkklasse heeft ook één enkele verantwoordelijkheid: zij drinkt! En over het algemeen is het woord ‘verantwoordelijkheid’ een uiterst vaag concept. Iemand is verantwoordelijk voor het lot van de mensheid, en iemand is verantwoordelijk voor het grootbrengen van de pinguïns die bij de paal zijn omgevallen.

Laten we twee implementaties van de drinker bekijken. De eerste, hierboven vermeld, bevat drie klassen: schenken, drinken en snacks.

De tweede is geschreven via de ‘Forward and Only Forward’-methodologie en bevat alle logica van de methode Handelen:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
с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 zien er, vanuit het gezichtspunt van een waarnemer van buitenaf, precies hetzelfde uit en delen dezelfde verantwoordelijkheid op het gebied van ‘drinken’.

Verwarring!

Vervolgens gaan we online en ontdekken een andere definitie van SRP: het Single Changeability Principle.

SCP stelt dat “Een module heeft maar één reden om te veranderen". Dat wil zeggen: “Verantwoordelijkheid is een reden voor verandering.”

(Het lijkt erop dat de jongens die met de oorspronkelijke definitie kwamen, vertrouwen hadden in de telepathische vermogens van de aapmens)

Nu valt alles op zijn plaats. Afzonderlijk kunnen we de schenk-, drink- en snackprocedures veranderen, maar in de drinker zelf kunnen we alleen de volgorde en samenstelling van de handelingen veranderen, bijvoorbeeld door de snack te verplaatsen voordat we gaan drinken of door het lezen van een toast toe te voegen.

Bij de ‘Voorwaarts en Alleen Voorwaarts’-benadering wordt alles wat veranderd kan worden alleen in de methode veranderd Handelen. Dit kan leesbaar en effectief zijn als er weinig logica is en het verandert zelden, maar het eindigt vaak in vreselijke methoden van elk 500 regels, met meer if-statements dan nodig is om Rusland tot de NAVO te laten toetreden.

Definitie 3. Lokalisatie van veranderingen.

Drinkers begrijpen vaak niet waarom ze wakker zijn geworden in het appartement van iemand anders, of waar hun mobiele telefoon is. Het is tijd om gedetailleerde logboekregistratie toe te voegen.

Laten we beginnen met loggen met het gietproces:

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

Door het in te kapselen Gietwerkinghebben we verstandig gehandeld vanuit het oogpunt van verantwoordelijkheid en inkapseling, maar nu worden we verward met het principe van variabiliteit. Naast de werking zelf, die kan veranderen, wordt ook de logging zelf veranderlijk. U zult een speciale logger voor het gieten moeten scheiden en aanmaken:

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

De nauwgezette lezer zal dat merken InloggenNa, LogVóór и OnFout kan ook individueel worden gewijzigd en zal, naar analogie met de vorige stappen, drie klassen creëren: GietLoggerBefore, PourLoggerAfter и PourErrorLogger.

En als we bedenken dat er drie operaties zijn voor een drinker, krijgen we negen houtkaplessen. Hierdoor bestaat de gehele drinkcirkel uit 14 (!!!) lessen.

Hyperbool? Nauwelijks! Een aapmens met een ontledingsgranaat zal de ‘schenker’ opsplitsen in een karaf, een glas, schenkoperatoren, een watervoorzieningsdienst, een fysiek model van de botsing van moleculen, en het komende kwartaal zal hij proberen de afhankelijkheden te ontwarren zonder globale variabelen. En geloof me, hij zal niet stoppen.

Het is op dit punt dat velen tot de conclusie komen dat SRP sprookjes zijn uit roze koninkrijken, en weggaan om noedels te spelen...

... zonder ooit te weten te komen over het bestaan ​​van een derde definitie van Srp:

“Het Single Responsibility Principle stelt dat dingen die op verandering lijken, moeten op één plek worden opgeslagen". of "Wat samen verandert, moet op één plek worden bewaard"

Dat wil zeggen: als we de registratie van een bewerking wijzigen, moeten we deze op één plek wijzigen.

Dit is een heel belangrijk punt - aangezien alle bovenstaande verklaringen van SRP zeiden dat het nodig was om de typen te verpletteren terwijl ze werden verpletterd, dat wil zeggen dat ze een "bovengrens" oplegden aan de grootte van het object, en nu we hebben het al over een “ondergrens” . Met andere woorden, SRP vereist niet alleen “verpletteren terwijl je verplettert”, maar ook om het niet te overdrijven – “verpletter geen in elkaar grijpende dingen”. Dit is de grote strijd tussen het scheermes van Occam en de aapmens!

Principe van één verantwoordelijkheid. Niet zo eenvoudig als het lijkt

Nu zou de drinker zich beter moeten voelen. Naast dat het niet nodig is om de IPourLogger logger in drie klassen op te delen, kunnen we ook alle loggers combineren in één type:

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

En als we een vierde type bewerking toevoegen, is het loggen ervan al klaar. En de code van de operaties zelf is schoon en vrij van infrastructuurruis.

Als gevolg hiervan hebben we 5 klassen voor het oplossen van het drankprobleem:

  • Gietoperatie
  • Drinkoperatie
  • Blokkerende werking
  • Logger
  • Drinker gevel

Elk van hen is strikt verantwoordelijk voor één functionaliteit en heeft één reden voor verandering. Alle regels die op verandering lijken, bevinden zich in de buurt.

Voorbeeld uit het echte leven

We hebben ooit een dienst geschreven voor het automatisch registreren van een b2b-klant. En er verscheen een GOD-methode voor 200 regels met vergelijkbare inhoud:

  • Ga naar 1C en maak een account aan
  • Ga met dit account naar de betaalmodule en maak het daar aan
  • Controleer of er geen account met een dergelijk account is aangemaakt op de hoofdserver
  • Maak een nieuw account aan
  • Voeg de inschrijfresultaten in de betaalmodule en het 1c-nummer toe aan de inschrijfresultatenservice
  • Voeg accountgegevens toe aan deze tabel
  • Maak voor deze klant een puntnummer aan in de puntenservice. Geef uw 1c-rekeningnummer door aan deze dienst.

En er stonden nog ongeveer 10 bedrijfsactiviteiten op deze lijst met vreselijke connectiviteit. Bijna iedereen had het accountobject nodig. Bij de helft van de oproepen waren de punt-ID en de klantnaam nodig.

Na een uur refactoring konden we de infrastructuurcode en enkele nuances van het werken met een account opsplitsen in afzonderlijke methoden/klassen. De God-methode maakte het gemakkelijker, maar er bleven nog 100 regels code over die maar niet ontward wilden worden.

Pas na een paar dagen werd duidelijk dat de essentie van deze “lichtgewicht” methode een bedrijfsalgoritme is. En dat de oorspronkelijke beschrijving van de technische specificaties behoorlijk complex was. En het is de poging om deze methode in stukken te breken die de SRP schendt, en niet andersom.

Formalisme.

Het is tijd om onze dronkaard met rust te laten. Droog je tranen - we zullen er zeker ooit op terugkomen. Laten we nu de kennis uit dit artikel formaliseren.

Formalisme 1. Definitie van SRP

  1. Scheid de elementen zodat elk van hen verantwoordelijk is voor één ding.
  2. Verantwoordelijkheid staat voor ‘reden om te veranderen’. Dat wil zeggen dat elk element slechts één reden voor verandering heeft, in termen van bedrijfslogica.
  3. Mogelijke veranderingen in de bedrijfslogica. moet gelokaliseerd zijn. Elementen die synchroon veranderen, moeten dichtbij zijn.

Formalisme 2. Noodzakelijke zelftestcriteria.

Ik heb niet voldoende criteria gezien om aan de SRP te voldoen. Maar er zijn noodzakelijke voorwaarden:

1) Vraag jezelf af wat deze klasse/methode/module/service doet. je moet het beantwoorden met een eenvoudige definitie. ( Bedankt Brightori )

uitleg

Soms is het echter erg moeilijk om een ​​eenvoudige definitie te vinden

2) Het oplossen van een bug of het toevoegen van een nieuwe functie heeft invloed op een minimaal aantal bestanden/klassen. Idealiter - één.

uitleg

Omdat de verantwoordelijkheid (voor een functie of bug) is samengevat in één bestand/klasse, weet u precies waar u moet zoeken en wat u moet bewerken. Bijvoorbeeld: voor de functie van het wijzigen van de uitvoer van logbewerkingen hoeft alleen de logger te worden gewijzigd. Het is niet nodig om de rest van de code door te nemen.

Een ander voorbeeld is het toevoegen van een nieuw UI-besturingselement, vergelijkbaar met de vorige. Als dit je dwingt om 10 verschillende entiteiten en 15 verschillende converters toe te voegen, lijkt het erop dat je overdrijft.

3) Als meerdere ontwikkelaars aan verschillende functies van uw project werken, is de kans op een samenvoegconflict, dat wil zeggen de kans dat hetzelfde bestand/dezelfde klasse tegelijkertijd door meerdere ontwikkelaars wordt gewijzigd, minimaal.

uitleg

Als je bij het toevoegen van een nieuwe operatie "Wodka onder de tafel gieten" de logger, de werking van drinken en schenken moet beïnvloeden, dan lijkt het erop dat de verantwoordelijkheden scheef zijn verdeeld. Natuurlijk is dit niet altijd mogelijk, maar we moeten proberen dit cijfer terug te dringen.

4) Wanneer u een verhelderende vraag over bedrijfslogica wordt gesteld (van een ontwikkelaar of manager), gaat u strikt naar één klasse/bestand en ontvangt u alleen van daaruit informatie.

uitleg

Functies, regels of algoritmen zijn compact geschreven, elk op één plek, en niet verspreid met vlaggen door de coderuimte.

5) De naamgeving is duidelijk.

uitleg

Onze klasse of methode is voor één ding verantwoordelijk, en die verantwoordelijkheid wordt weerspiegeld in de naam ervan

AllManagersManagerService - hoogstwaarschijnlijk een God-klasse
Lokale betaling - waarschijnlijk niet

Formalisme 3. Occam-eerste ontwikkelingsmethodologie.

Aan het begin van het ontwerp kent en voelt de aapmens niet alle subtiliteiten van het opgeloste probleem en kan hij een fout maken. Fouten kun je op verschillende manieren maken:

  • Maak objecten te groot door verschillende verantwoordelijkheden samen te voegen
  • Herkaderen door een enkele verantwoordelijkheid in veel verschillende typen te verdelen
  • Definieer de grenzen van verantwoordelijkheid verkeerd

Het is belangrijk om de regel te onthouden: ‘het is beter om een ​​grote fout te maken’ of ‘als je het niet zeker weet, splits het dan niet op’. Als uw klas bijvoorbeeld twee verantwoordelijkheden bevat, is dit nog steeds begrijpelijk en kan deze in tweeën worden gesplitst met minimale wijzigingen in de clientcode. Het samenstellen van een glas uit glasscherven is doorgaans lastiger omdat de context over meerdere bestanden verspreid is en omdat de noodzakelijke afhankelijkheden in de clientcode ontbreken.

Het is tijd om er een einde aan te maken

De reikwijdte van SRP is niet beperkt tot OOP en SOLID. Het is van toepassing op methoden, functies, klassen, modules, microservices en services. Het is van toepassing op zowel de “Figax-Figax-and-Prod”- als de “Raket-Science”-ontwikkeling, waardoor de wereld overal een beetje beter wordt. Als je erover nadenkt, is dit bijna het fundamentele principe van alle techniek. Werktuigbouwkunde, besturingssystemen en eigenlijk alle complexe systemen zijn opgebouwd uit componenten, en ‘onderfragmentatie’ berooft ontwerpers van flexibiliteit, ‘overfragmentatie’ berooft ontwerpers van efficiëntie, en onjuiste grenzen beroven hen van rede en gemoedsrust.

Principe van één verantwoordelijkheid. Niet zo eenvoudig als het lijkt

SRP is niet van nature uitgevonden en maakt geen deel uit van de exacte wetenschap. Het doorbreekt onze biologische en psychologische beperkingen en is slechts een manier om complexe systemen te controleren en te ontwikkelen met behulp van het brein van de aapmens. Hij vertelt ons hoe we een systeem kunnen ontbinden. De oorspronkelijke formulering vereiste een behoorlijke hoeveelheid telepathie, maar ik hoop dat dit artikel een deel van het rookgordijn opruimt.

Bron: www.habr.com

Voeg een reactie