Parimi i Përgjegjësisë së Vetëm. Jo aq e thjeshtë sa duket

Parimi i Përgjegjësisë së Vetëm. Jo aq e thjeshtë sa duket Parimi i përgjegjësisë së vetme, i njohur gjithashtu si parimi i përgjegjësisë së vetme,
aka parimi i ndryshueshmërisë uniforme - një djalë jashtëzakonisht i rrëshqitshëm për t'u kuptuar dhe një pyetje kaq nervoze në një intervistë programuesi.

Njohja ime e parë serioze me këtë parim u bë në fillim të vitit të parë, kur të rinjtë dhe të gjelbërt i çuan në pyll për të bërë studentë të vërtetë nga larvat.

Në pyll, ne u ndamë në grupe me nga 8-9 persona secili dhe bëmë një konkurs - cili grup do të pinte një shishe vodka më shpejt, me kusht që personi i parë nga grupi të derdhë vodka në një gotë, i dyti ta pijë atë, dhe i treti ka një meze të lehtë. Njësia që ka përfunduar funksionimin e saj kalon në fund të radhës së grupit.

Rasti kur madhësia e radhës ishte shumëfish i tre ishte një zbatim i mirë i SRP.

Përkufizim 1. Përgjegjësi e vetme.

Përkufizimi zyrtar i Parimit të Përgjegjësisë së Vetëm (SRP) thotë se çdo subjekt ka përgjegjësinë dhe arsyen e tij të ekzistencës dhe ka vetëm një përgjegjësi.

Konsideroni objektin "Pija" (Tippler).
Për të zbatuar parimin SRP, ne do t'i ndajmë përgjegjësitë në tre:

  • Një derdh (PourOperation)
  • Një pi (DrinkUpOperation)
  • Njëra ka një meze të lehtë (TakeBiteOperation)

Secili prej pjesëmarrësve në proces është përgjegjës për një komponent të procesit, domethënë ka një përgjegjësi atomike - për të pirë, derdhur ose meze të lehtë.

Vrima e pijes, nga ana tjetër, është një fasadë për këto operacione:

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

Parimi i Përgjegjësisë së Vetëm. Jo aq e thjeshtë sa duket

Pse?

Programuesi i njeriut shkruan kodin për njeriun majmun, dhe njeriu majmun është i pavëmendshëm, budalla dhe gjithmonë me nxitim. Ai mund të mbajë dhe të kuptojë rreth 3-7 terma njëherësh.
Në rastin e një pijanec, ka tre nga këto terma. Mirëpo, nëse kodin e shkruajmë me një fletë, atëherë ai do të përmbajë duar, syze, zënka dhe debate të pafundme për politikën. Dhe e gjithë kjo do të jetë në trupin e një metode. Unë jam i sigurt se ju keni parë një kod të tillë në praktikën tuaj. Nuk është testi më human për psikikën.

Nga ana tjetër, njeriu majmun është krijuar për të simuluar objekte të botës reale në kokën e tij. Në imagjinatën e tij, ai mund t'i shtyjë ato së bashku, të mbledhë objekte të reja prej tyre dhe t'i çmontojë ato në të njëjtën mënyrë. Imagjinoni një makinë të modelit të vjetër. Në imagjinatën tuaj, ju mund të hapni derën, të hiqni veshjen e derës dhe të shihni atje mekanizmat e ngritjes së dritares, brenda të cilave do të ketë ingranazhe. Por ju nuk mund t'i shihni të gjithë komponentët e makinës në të njëjtën kohë, në një "listë". Të paktën "njeriu majmun" nuk mundet.

Prandaj, programuesit njerëzorë zbërthejnë mekanizmat kompleksë në një grup elementësh më pak kompleksë dhe funksionalë. Sidoqoftë, ai mund të dekompozohet në mënyra të ndryshme: në shumë makina të vjetra, kanali i ajrit hyn në derë, dhe në makinat moderne, një dështim në elektronikën e bllokimit pengon motorin të fillojë, gjë që mund të jetë problem gjatë riparimeve.

Kështu që, SRP është një parim që shpjegon SI të zbërthehet, domethënë ku të vizatoni vijën ndarëse.

Ai thotë se është e nevojshme të zbërthehet sipas parimit të ndarjes së "përgjegjësisë", domethënë sipas detyrave të objekteve të caktuara.

Parimi i Përgjegjësisë së Vetëm. Jo aq e thjeshtë sa duket

Le të kthehemi te pirja dhe avantazhet që merr njeriu majmun gjatë dekompozimit:

  • Kodi është bërë jashtëzakonisht i qartë në çdo nivel
  • Kodi mund të shkruhet nga disa programues njëherësh (secili shkruan një element të veçantë)
  • Testimi i automatizuar është thjeshtuar - sa më i thjeshtë të jetë elementi, aq më i lehtë është të testohet
  • Përbërja e kodit shfaqet - mund ta zëvendësoni DrinkUpOperation në një operacion në të cilin një pijanec derdh lëngje nën tryezë. Ose zëvendësoni operacionin e derdhjes me një operacion në të cilin përzieni verë me ujë ose vodka dhe birrë. Në varësi të kërkesave të biznesit, mund të bëni gjithçka pa prekur kodin e metodës Tippler.Akt.
  • Nga këto veprime mund ta palosni grykësin (vetëm duke përdorur TakeBitOperation), Alkoolike (përdorim vetëm DrinkUpOperation direkt nga shishja) dhe plotësojnë shumë kërkesa të tjera të biznesit.

(Oh, duket se ky është tashmë një parim OCP, dhe unë kam shkelur përgjegjësinë e këtij postimi)

Dhe, natyrisht, disavantazhet:

  • Ne do të duhet të krijojmë më shumë lloje.
  • Një pijanec pi për herë të parë disa orë më vonë se sa do të pinte ndryshe.

Përkufizimi 2. Ndryshueshmëria e unifikuar.

Më lejoni, zotërinj! Klasa e pijeve ka gjithashtu një përgjegjësi të vetme - pi! Dhe në përgjithësi, fjala "përgjegjësi" është një koncept jashtëzakonisht i paqartë. Dikush është përgjegjës për fatin e njerëzimit, dhe dikush është përgjegjës për rritjen e pinguinëve që u përmbysën në shtyllë.

Le të shqyrtojmë dy zbatime të pijes. E para, e përmendur më lart, përmban tre klasa - derdhje, pije dhe meze të lehtë.

E dyta është shkruar përmes metodologjisë "Përpara dhe vetëm përpara" dhe përmban të gjithë logjikën në metodë akt:

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

Të dyja këto klasa, nga këndvështrimi i një vëzhguesi të jashtëm, duken saktësisht të njëjta dhe ndajnë të njëjtën përgjegjësi të "pirjes".

Konfuzion!

Pastaj ne shkojmë në internet dhe zbulojmë një përkufizim tjetër të SRP - Parimi i vetëm i ndryshueshmërisë.

SCP thekson se "Një modul ka një dhe vetëm një arsye për të ndryshuar". Kjo do të thotë, "Përgjegjësia është një arsye për ndryshim".

(Duket se djemtë që dolën me përkufizimin origjinal ishin të sigurt në aftësitë telepatike të majmunit)

Tani gjithçka bie në vend. Më vete, ne mund të ndryshojmë procedurat e derdhjes, të pirjes dhe të ngrënitjes, por në vetë pijen mund të ndryshojmë vetëm sekuencën dhe përbërjen e veprimeve, për shembull, duke lëvizur ushqimin përpara se të pimë ose duke shtuar leximin e një dolli.

Në qasjen "Përpara dhe vetëm përpara", gjithçka që mund të ndryshohet ndryshohet vetëm në metodë akt. Kjo mund të jetë e lexueshme dhe efektive kur ka pak logjikë dhe ndryshon rrallë, por shpesh përfundon në metoda të tmerrshme prej 500 rreshtash secila, me më shumë nëse-deklarata sesa kërkohet që Rusia të anëtarësohet në NATO.

Përkufizimi 3. Lokalizimi i ndryshimeve.

Ata që pinë shpesh nuk e kuptojnë pse janë zgjuar në banesën e dikujt tjetër, apo ku është celulari i tyre. Është koha për të shtuar regjistrime të detajuara.

Le të fillojmë regjistrimin me procesin e derdhjes:

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

Duke e kapsuluar atë në PourOperation, kemi vepruar me mençuri nga pikëpamja e përgjegjësisë dhe e kapsulimit, por tani jemi ngatërruar me parimin e ndryshueshmërisë. Përveç vetë operacionit, i cili mund të ndryshojë, vetë prerjet gjithashtu bëhen të ndryshueshme. Ju do të duhet të ndani dhe të krijoni një regjistrues të veçantë për operacionin e derdhjes:

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

Lexuesi i përpiktë do ta vërejë këtë LogPas, Identifikohu Para и NjëGabim gjithashtu mund të ndryshohet individualisht dhe, në analogji me hapat e mëparshëm, do të krijojë tre klasa: PourLoggerPara, PourLoggerPas и PourErrorLogger.

Dhe duke kujtuar se ka tre operacione për një pijanec, marrim nëntë klasa prerje. Si rezultat, i gjithë rrethi i pijes përbëhet nga 14 (!!!) klasa.

Hiperbola? Vështirë! Një njeri majmun me një granatë dekompozimi do të ndajë "derdhësin" në një dekantues, një gotë, operatorë derdhjeje, një shërbim të furnizimit me ujë, një model fizik të përplasjes së molekulave dhe për tremujorin e ardhshëm ai do të përpiqet të zgjidhë varësitë pa variablat globale. Dhe më besoni, ai nuk do të ndalet.

Pikërisht në këtë pikë shumë vijnë në përfundimin se SRP janë përralla nga mbretëritë rozë dhe largohen për të luajtur petë...

... pa mësuar ndonjëherë për ekzistencën e një përkufizimi të tretë të Srp:

“Parimi i Përgjegjësisë së vetme thotë se gjërat që janë të ngjashme me ndryshimin duhet të ruhen në një vend". ose "Çfarë ndryshimesh së bashku duhet të mbahen në një vend"

Kjo do të thotë, nëse ndryshojmë regjistrimin e një operacioni, atëherë duhet ta ndryshojmë atë në një vend.

Kjo është një pikë shumë e rëndësishme - pasi të gjitha shpjegimet e SRP që u përmendën më lart thanë se ishte e nevojshme të shtypeshin llojet gjatë grimcimit, domethënë ata vendosën një "kufi të sipërm" në madhësinë e objektit, dhe tani ne tashmë po flasim për një "kufi më të ulët". Me fjale te tjera, SRP jo vetëm që kërkon "shtypjen gjatë shtypjes", por edhe të mos e teproni - "mos i shtypni gjërat e ndërlidhura". Kjo është beteja e madhe mes briskut të Okamit dhe majmunit!

Parimi i Përgjegjësisë së Vetëm. Jo aq e thjeshtë sa duket

Tani pirësi duhet të ndihet më mirë. Përveç faktit që nuk ka nevojë të ndajmë logger-in IPourLogger në tre klasa, ne gjithashtu mund të kombinojmë të gjithë regjistruesit në një lloj:

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

Dhe nëse shtojmë një lloj të katërt të operacionit, atëherë prerjet për të janë tashmë gati. Dhe vetë kodi i operacioneve është i pastër dhe pa zhurmë infrastrukturore.

Si rezultat, ne kemi 5 klasa për zgjidhjen e problemit të pijes:

  • Operacioni i derdhjes
  • Operacioni i pirjes
  • Operacioni i bllokimit
  • Logger
  • Fasada e pijeve

Secili prej tyre është përgjegjës rreptësisht për një funksionalitet dhe ka një arsye për ndryshim. Të gjitha rregullat e ngjashme me ndryshimin ndodhen afër.

Shembull i jetës reale

Dikur kemi shkruar një shërbim për regjistrimin automatik të një klienti b2b. Dhe një metodë e Zotit u shfaq për 200 rreshta me përmbajtje të ngjashme:

  • Shkoni te 1C dhe krijoni një llogari
  • Me këtë llogari, shkoni te moduli i pagesës dhe krijoni atë atje
  • Kontrolloni që një llogari me një llogari të tillë të mos jetë krijuar në serverin kryesor
  • Krijo nje llogari te re
  • Shtoni rezultatet e regjistrimit në modulin e pagesës dhe numrin 1c në shërbimin e rezultateve të regjistrimit
  • Shtoni informacionin e llogarisë në këtë tabelë
  • Krijo një numër pikësh për këtë klient në shërbimin e pikës. Kaloni numrin tuaj të llogarisë 1c në këtë shërbim.

Dhe kishte rreth 10 operacione të tjera biznesi në këtë listë me lidhje të tmerrshme. Pothuajse të gjithë kishin nevojë për objektin e llogarisë. Në gjysmën e telefonatave nevojiteshin ID-ja e pikës dhe emri i klientit.

Pas një ore rifaktorimi, ne mundëm të veçonim kodin e infrastrukturës dhe disa nga nuancat e punës me një llogari në metoda/klasa të veçanta. Metoda e Zotit e bëri më të lehtë, por kishin mbetur 100 rreshta kodi që thjesht nuk donin të zgjidheshin.

Vetëm pas disa ditësh u bë e qartë se thelbi i kësaj metode "të lehtë" është një algoritëm biznesi. Dhe se përshkrimi origjinal i specifikimeve teknike ishte mjaft kompleks. Dhe është përpjekja për ta thyer këtë metodë në copa që do të shkelë SRP, dhe jo anasjelltas.

Formalizmi.

Është koha të lëmë të dehurin tonë të qetë. Thajini lotët - ne patjetër do t'i kthehemi një ditë. Tani le të zyrtarizojmë njohuritë nga ky artikull.

Formalizmi 1. Përkufizimi i SRP

  1. Ndani elementët në mënyrë që secili prej tyre të jetë përgjegjës për një gjë.
  2. Përgjegjësia do të thotë "arsye për të ndryshuar". Domethënë, çdo element ka vetëm një arsye për ndryshim, për sa i përket logjikës së biznesit.
  3. Ndryshime të mundshme në logjikën e biznesit. duhet të lokalizohet. Elementet që ndryshojnë në mënyrë sinkrone duhet të jenë afër.

Formalizmi 2. Kriteret e nevojshme të vetëtestimit.

Nuk kam parë kritere të mjaftueshme për përmbushjen e SRP. Por ka kushte të nevojshme:

1) Pyesni veten se çfarë bën kjo klasë/metodë/modul/shërbim. duhet t'i përgjigjeni me një përkufizim të thjeshtë. ( Faleminderit Brightori )

shpjegimet

Megjithatë, ndonjëherë është shumë e vështirë të gjesh një përkufizim të thjeshtë

2) Rregullimi i një gabimi ose shtimi i një veçorie të re ndikon në një numër minimal skedarësh/klasash. Idealisht - një.

shpjegimet

Meqenëse përgjegjësia (për një veçori ose gabim) është e përmbledhur në një skedar/klasë, ju e dini saktësisht se ku të shikoni dhe çfarë të redaktoni. Për shembull: veçoria e ndryshimit të prodhimit të operacioneve të logging do të kërkojë ndryshimin vetëm të loggerit. Nuk ka nevojë të kaloni nëpër pjesën tjetër të kodit.

Një shembull tjetër është shtimi i një kontrolli të ri UI, i ngjashëm me ato të mëparshme. Nëse kjo ju detyron të shtoni 10 entitete të ndryshme dhe 15 konvertues të ndryshëm, duket sikur po e teproni.

3) Nëse disa zhvillues janë duke punuar në veçori të ndryshme të projektit tuaj, atëherë gjasat e një konflikti të bashkimit, domethënë, gjasat që i njëjti skedar/klasë të ndryshohet nga disa zhvillues në të njëjtën kohë, është minimale.

shpjegimet

Nëse, kur shtoni një operacion të ri "Hidh vodka nën tavolinë", duhet të ndikoni në prerësin, funksionimin e pirjes dhe derdhjes, atëherë duket se përgjegjësitë janë të ndara shtrembër. Sigurisht, kjo nuk është gjithmonë e mundur, por ne duhet të përpiqemi ta zvogëlojmë këtë shifër.

4) Kur ju bëhet një pyetje sqaruese në lidhje me logjikën e biznesit (nga një zhvillues ose menaxher), ju hyni rreptësisht në një klasë/skedar dhe merrni informacion vetëm prej andej.

shpjegimet

Karakteristikat, rregullat ose algoritmet janë shkruar në mënyrë kompakte, secila në një vend, dhe jo të shpërndara me flamuj në të gjithë hapësirën e kodit.

5) Emërtimi është i qartë.

shpjegimet

Klasa ose metoda jonë është përgjegjëse për një gjë dhe përgjegjësia pasqyrohet në emrin e saj

AllManagersManagerService - ka shumë të ngjarë një klasë e Zotit
LocalPayment - ndoshta jo

Formalizmi 3. Metodologjia e zhvillimit të parë Occam.

Në fillim të projektimit, njeriu majmun nuk i njeh dhe nuk i ndjen të gjitha hollësitë e problemit që zgjidhet dhe mund të bëjë një gabim. Ju mund të bëni gabime në mënyra të ndryshme:

  • Bëjini objektet shumë të mëdha duke bashkuar përgjegjësi të ndryshme
  • Riformulimi duke e ndarë një përgjegjësi të vetme në shumë lloje të ndryshme
  • Përcaktoni gabimisht kufijtë e përgjegjësisë

Është e rëndësishme të mbani mend rregullin: "është më mirë të bëni një gabim të madh" ose "nëse nuk jeni të sigurt, mos e ndani". Nëse, për shembull, klasa juaj përmban dy përgjegjësi, atëherë ajo është ende e kuptueshme dhe mund të ndahet në dysh me ndryshime minimale në kodin e klientit. Montimi i një gote nga copa xhami është zakonisht më i vështirë për shkak të përhapjes së kontekstit në disa skedarë dhe mungesës së varësive të nevojshme në kodin e klientit.

Është koha për ta quajtur atë një ditë

Shtrirja e SRP nuk është e kufizuar në OOP dhe SOLID. Ai zbatohet për metodat, funksionet, klasat, modulet, mikroshërbimet dhe shërbimet. Ai zbatohet si për zhvillimin e "figax-figax-and-prod" dhe "rocket-science", duke e bërë botën pak më të mirë kudo. Nëse mendoni për këtë, ky është pothuajse parimi themelor i të gjithë inxhinierisë. Inxhinieria mekanike, sistemet e kontrollit dhe në të vërtetë të gjitha sistemet komplekse janë ndërtuar nga komponentë, dhe "nënfragmentimi" i privon projektuesit nga fleksibiliteti, "mbifragmentimi" i privon projektuesit nga efikasiteti dhe kufijtë e pasaktë i privojnë ata nga arsyeja dhe qetësia e mendjes.

Parimi i Përgjegjësisë së Vetëm. Jo aq e thjeshtë sa duket

SRP nuk është shpikur nga natyra dhe nuk është pjesë e shkencës ekzakte. Ai del nga kufizimet tona biologjike dhe psikologjike.Është vetëm një mënyrë për të kontrolluar dhe zhvilluar sisteme komplekse duke përdorur trurin e njeriut të majmunit. Ai na tregon se si të dekompozojmë një sistem. Formulimi origjinal kërkonte një sasi të mjaftueshme telepatie, por shpresoj që ky artikull të pastrojë një pjesë të tymit.

Burimi: www.habr.com

Shto një koment