Principen om ett enda ansvar. Inte så enkelt som det verkar

Principen om ett enda ansvar. Inte så enkelt som det verkar Principen om ett enda ansvar, även känd som principen om ett enda ansvar,
aka principen om enhetlig variabilitet - en extremt hal kille att förstå och en så nervös fråga vid en programmerarintervju.

Min första seriösa bekantskap med denna princip ägde rum i början av första året, då de unga och gröna togs till skogen för att göra elever av larver - riktiga studenter.

I skogen delades vi in ​​i grupper om 8-9 personer vardera och hade en tävling - vilken grupp som skulle dricka en flaska vodka snabbast, förutsatt att den första personen från gruppen häller upp vodka i ett glas, den andra dricker den, och den tredje har ett mellanmål. Enheten som har avslutat sin verksamhet flyttar till slutet av gruppens kö.

Fallet där köstorleken var en multipel av tre var en bra implementering av SRP.

Definition 1. Enskilt ansvar.

Den officiella definitionen av Single Responsibility Principle (SRP) säger att varje enhet har sitt eget ansvar och existensgrund och den har bara ett ansvar.

Betrakta objektet "Dricker" (Tippler).
För att implementera SRP-principen kommer vi att dela upp ansvarsområden i tre:

  • En häller (Hälloperation)
  • En dricker (DrinkUp Operation)
  • En har ett mellanmål (TakeBiteOperation)

Var och en av deltagarna i processen är ansvarig för en komponent i processen, det vill säga har ett atomärt ansvar - att dricka, hälla eller snacka.

Drickshålet är i sin tur en fasad för dessa operationer:

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

Principen om ett enda ansvar. Inte så enkelt som det verkar

Varför?

Den mänskliga programmeraren skriver kod för apmänniskan, och apmänniskan är ouppmärksam, dum och har alltid bråttom. Han kan hålla och förstå ungefär 3 - 7 termer samtidigt.
När det gäller en fyllare finns det tre av dessa termer. Men om vi skriver koden med ett ark, kommer den att innehålla händer, glasögon, slagsmål och oändliga argument om politik. Och allt detta kommer att finnas i kroppen av en metod. Jag är säker på att du har sett sådan kod i din praktik. Inte det mest humana testet för psyket.

Å andra sidan är apmannen designad för att simulera verkliga objekt i hans huvud. I sin fantasi kan han trycka ihop dem, sätta ihop nya föremål från dem och plocka isär dem på samma sätt. Föreställ dig en gammal modellbil. I din fantasi kan du öppna dörren, skruva loss dörrbeklädnaden och se där fönsterhissmekanismerna, inuti vilka det kommer att finnas växlar. Men du kan inte se alla komponenter i maskinen samtidigt, i en "listning". Åtminstone kan "apmannen" inte.

Därför bryter mänskliga programmerare ner komplexa mekanismer till en uppsättning mindre komplexa och fungerande element. Det kan dock brytas ner på olika sätt: i många gamla bilar går luftkanalen in i dörren och i moderna bilar hindrar ett fel i låselektroniken att motorn startar, vilket kan vara ett problem vid reparationer.

nu, SRP är en princip som förklarar HUR man bryts ner, det vill säga var man drar skiljelinjen.

Han säger att det är nödvändigt att bryta ner enligt principen om uppdelning av "ansvar", det vill säga enligt uppgifterna för vissa objekt.

Principen om ett enda ansvar. Inte så enkelt som det verkar

Låt oss återgå till drickandet och fördelarna som apmannen får under nedbrytningen:

  • Koden har blivit extremt tydlig på alla nivåer
  • Koden kan skrivas av flera programmerare samtidigt (var och en skriver ett separat element)
  • Automatiserad testning förenklas - ju enklare element, desto lättare är det att testa
  • Kodens sammansättning visas - du kan byta ut DrinkUp Operation till en operation där en fyllare häller vätska under bordet. Eller byt ut hälloperationen med en operation där du blandar vin och vatten eller vodka och öl. Beroende på affärskrav kan du göra allt utan att röra metodkoden Tippler.Act.
  • Från dessa operationer kan du vika frossaren (med endast TakeBitOperation), Alkoholhaltig (endast använda DrinkUp Operation direkt från flaskan) och uppfyller många andra affärskrav.

(Åh, det verkar som att detta redan är en OCP-princip, och jag har brutit mot ansvaret för detta inlägg)

Och, naturligtvis, nackdelarna:

  • Vi måste skapa fler typer.
  • En fyllare dricker för första gången ett par timmar senare än han annars skulle ha gjort.

Definition 2. Unified variabilitet.

Tillåt mig, mina herrar! Dricksklassen har också ett enda ansvar – den dricker! Och i allmänhet är ordet "ansvar" ett extremt vagt begrepp. Någon är ansvarig för mänsklighetens öde, och någon är ansvarig för att höja pingvinerna som välte vid polen.

Låt oss överväga två implementeringar av drinkaren. Den första, som nämns ovan, innehåller tre klasser - häll, drick och mellanmål.

Den andra är skriven genom metoden "Forward and Only Forward" och innehåller all logik i metoden Agera:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
с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);
    }
   }
}

Båda dessa klasser, från en utomstående observatörs synvinkel, ser exakt likadana ut och delar samma ansvar att "dricka".

Förvirring!

Sedan går vi online och tar reda på en annan definition av SRP - Single Changeability Principle.

SCP säger att "En modul har en och bara en anledning att byta". Det vill säga "Ansvar är en anledning till förändring."

(Det verkar som att killarna som kom fram till den ursprungliga definitionen var säkra på apmannens telepatiska förmågor)

Nu faller allt på plats. Separat kan vi ändra procedurerna för att hälla, dricka och äta mellanmål, men i själva drickaren kan vi bara ändra sekvensen och sammansättningen av operationer, till exempel genom att flytta mellanmålet innan vi dricker eller lägga till avläsningen av en toast.

I metoden "Forward and Only Forward" ändras allt som kan ändras endast i metoden Agera. Detta kan vara läsbart och effektivt när det finns lite logik och det ändras sällan, men ofta hamnar det i fruktansvärda metoder på 500 rader vardera, med fler om-uttalanden än vad som krävs för att Ryssland ska gå med i Nato.

Definition 3. Lokalisering av ändringar.

Drickare förstår ofta inte varför de vaknade i någon annans lägenhet, eller var deras mobiltelefon är. Det är dags att lägga till detaljerad loggning.

Låt oss börja logga med hällningsprocessen:

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

Genom att kapsla in det Hälloperation, vi agerade klokt ur ansvars- och inkapslingssynpunkt, men nu är vi förväxlade med principen om föränderlighet. Förutom själva operationen, som kan förändras, blir själva loggningen också föränderlig. Du måste separera och skapa en speciell logger för hälloperationen:

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

Det kommer den noggranna läsaren att märka Logga efter, Logga före и OnError kan också ändras individuellt och, i analogi med de föregående stegen, kommer tre klasser att skapas: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

Och med tanke på att det finns tre operationer för en drinkare, får vi nio loggarklasser. Som ett resultat består hela dryckescirkeln av 14 (!!!) klasser.

Hyperbel? Knappast! En apman med en sönderfallsgranat kommer att dela upp "hällaren" i en karaff, ett glas, hälloperatorer, en vattenförsörjningstjänst, en fysisk modell av kollisionen av molekyler, och under nästa kvartal kommer han att försöka reda ut beroenden utan globala variabler. Och tro mig, han kommer inte att sluta.

Det är vid det här laget som många kommer till slutsatsen att SRP är sagor från rosa kungadömen, och åker iväg för att spela nudlar...

... utan att någonsin lära mig om existensen av en tredje definition av Srp:

"Principen om ett enda ansvar säger det saker som liknar förändring bör lagras på ett ställe". eller "Det som förändras tillsammans bör hållas på ett ställe"

Det vill säga, om vi ändrar loggningen av en operation måste vi ändra den på ett ställe.

Detta är en mycket viktig punkt - eftersom alla förklaringar av SRP som var ovan sa att det var nödvändigt att krossa typerna medan de krossades, det vill säga de satte en "övre gräns" för storleken på föremålet, och nu vi talar redan om en "nedre gräns" . Med andra ord, SRP kräver inte bara "krossning under krossning", utan också att inte överdriva det - "krossa inte sammankopplade saker". Det här är den stora striden mellan Occams rakkniv och apmannen!

Principen om ett enda ansvar. Inte så enkelt som det verkar

Nu borde den som dricker må bättre. Förutom det faktum att det inte finns något behov av att dela upp IPourLogger-loggern i tre klasser, kan vi också kombinera alla loggare till en typ:

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

Och om vi lägger till en fjärde typ av operation, är loggningen för den redan klar. Och koden för själva verksamheten är ren och fri från infrastrukturbuller.

Som ett resultat har vi 5 klasser för att lösa alkoholproblemet:

  • Hälloperation
  • Dricksoperation
  • Jamming operation
  • Logger
  • Drinker fasad

Var och en av dem ansvarar strikt för en funktionalitet och har en anledning till förändring. Alla regler som liknar förändring finns i närheten.

Exempel i verkligheten

Vi skrev en gång en tjänst för automatisk registrering av en b2b-klient. Och en GUD-metod dök upp för 200 rader med liknande innehåll:

  • Gå till 1C och skapa ett konto
  • Med detta konto, gå till betalningsmodulen och skapa den där
  • Kontrollera att ett konto med ett sådant konto inte har skapats på huvudservern
  • Skapa ett nytt konto
  • Lägg till registreringsresultaten i betalningsmodulen och 1c-numret till registreringsresultattjänsten
  • Lägg till kontoinformation i den här tabellen
  • Skapa ett poängnummer för denna klient i punkttjänsten. Skicka ditt 1c-kontonummer till den här tjänsten.

Och det fanns cirka 10 fler affärsverksamheter på den här listan med fruktansvärda anslutningar. Nästan alla behövde kontoobjektet. Punkt-ID och klientnamn behövdes i hälften av samtalen.

Efter en timmes refaktorering kunde vi separera infrastrukturkoden och några av nyanserna av att arbeta med ett konto i separata metoder/klasser. Gud-metoden gjorde det enklare, men det fanns 100 rader kod kvar som bara inte ville lösas upp.

Först efter några dagar stod det klart att kärnan i denna "lätta" metod är en affärsalgoritm. Och att den ursprungliga beskrivningen av de tekniska specifikationerna var ganska komplex. Och det är försöket att bryta denna metod i bitar som kommer att bryta mot SRP, och inte vice versa.

Formalism.

Det är dags att lämna vårt berusade ifred. Torka dina tårar - vi kommer definitivt tillbaka till det någon gång. Låt oss nu formalisera kunskapen från den här artikeln.

Formalism 1. Definition av SRP

  1. Separera elementen så att var och en av dem är ansvarig för en sak.
  2. Ansvar står för "anledning till förändring". Det vill säga att varje element har bara en anledning till förändring, i termer av affärslogik.
  3. Potentiella förändringar av affärslogik. måste vara lokaliserad. Element som ändras synkront måste finnas i närheten.

Formalism 2. Nödvändiga självtestkriterier.

Jag har inte sett tillräckliga kriterier för att uppfylla SRP. Men det finns nödvändiga villkor:

1) Fråga dig själv vad denna klass/metod/modul/tjänst gör. du måste svara på det med en enkel definition. ( Tack Brightori )

förklaringar

Men ibland är det väldigt svårt att hitta en enkel definition

2) Att fixa en bugg eller lägga till en ny funktion påverkar ett minsta antal filer/klasser. Helst - en.

förklaringar

Eftersom ansvaret (för en funktion eller bugg) är inkapslat i en fil/klass vet du exakt var du ska leta och vad du ska redigera. Till exempel: funktionen att ändra utdata från loggningsoperationer kräver att endast loggern ändras. Det finns ingen anledning att köra igenom resten av koden.

Ett annat exempel är att lägga till en ny UI-kontroll, liknande de tidigare. Om detta tvingar dig att lägga till 10 olika enheter och 15 olika omvandlare, ser det ut som att du överdriver det.

3) Om flera utvecklare arbetar med olika funktioner i ditt projekt är sannolikheten för en sammanslagningskonflikt, det vill säga sannolikheten att samma fil/klass kommer att ändras av flera utvecklare samtidigt, minimal.

förklaringar

Om du, när du lägger till en ny operation "Häll vodka under bordet", måste påverka loggern, driften av att dricka och hälla, ser det ut som att ansvaret är snett uppdelat. Det är naturligtvis inte alltid möjligt, men vi bör försöka minska denna siffra.

4) När du ställer en förtydligande fråga om affärslogik (från en utvecklare eller chef) går du strikt in i en klass/fil och får endast information därifrån.

förklaringar

Funktioner, regler eller algoritmer skrivs kompakt, var och en på ett ställe, och inte utspridda med flaggor i hela kodutrymmet.

5) Namnet är tydligt.

förklaringar

Vår klass eller metod är ansvarig för en sak, och ansvaret återspeglas i dess namn

AllManagersManagerService - troligen en gudsklass
LocalPayment - förmodligen inte

Formalism 3. Occam-first development methodology.

I början av designen vet apamannen inte och känner inte alla subtiliteter av problemet löst och kan göra ett misstag. Du kan göra misstag på olika sätt:

  • Gör objekt för stora genom att slå samman olika ansvarsområden
  • Reframing genom att dela upp ett enda ansvar i många olika typer
  • Definiera gränserna för ansvar felaktigt

Det är viktigt att komma ihåg regeln: "det är bättre att göra ett stort misstag" eller "om du inte är säker, dela inte upp det." Om din klass till exempel innehåller två ansvarsområden är det fortfarande förståeligt och kan delas upp i två med minimala ändringar av klientkoden. Att sätta ihop ett glas från glassplitter är vanligtvis svårare på grund av att sammanhanget är spritt över flera filer och avsaknaden av nödvändiga beroenden i klientkoden.

Det är dags att kalla det en dag

Omfattningen av SRP är inte begränsad till OOP och SOLID. Det gäller metoder, funktioner, klasser, moduler, mikrotjänster och tjänster. Det gäller både "figax-figax-and-prod" och "rocket-science" utveckling, vilket gör världen lite bättre överallt. Om du tänker efter är detta nästan den grundläggande principen för all ingenjörskonst. Maskinteknik, kontrollsystem och faktiskt alla komplexa system är byggda av komponenter, och "underfragmentering" berövar konstruktörer flexibilitet, "överfragmentering" berövar konstruktörer effektivitet, och felaktiga gränser berövar dem förnuft och sinnesfrid.

Principen om ett enda ansvar. Inte så enkelt som det verkar

SRP är inte uppfunnet av naturen och är inte en del av exakt vetenskap. Det bryter ut ur våra biologiska och psykologiska begränsningar. Det är bara ett sätt att kontrollera och utveckla komplexa system med hjälp av apmänniskans hjärna. Han berättar för oss hur man bryter ner ett system. Den ursprungliga formuleringen krävde en hel del telepati, men jag hoppas att den här artikeln rensar en del av rökridån.

Källa: will.com

Lägg en kommentar