Egységes Felelősség Elve. Nem annyira egyszerű, mint amilyennek tűnik

Egységes Felelősség Elve. Nem annyira egyszerű, mint amilyennek tűnik Egységes felelősség elve, azaz az egységes felelősség elve,
más néven az egységes változók elve — rendkívül nehezen érthető dolog és egy nagyon izgalmas kérdés a programozói interjúkon.

Az első komoly találkozásom ezzel az elvvel az első év elején történt, amikor fiatal és tapasztalatlan diákokat elvittek az erdőbe, hogy a csótányokból igazi diákokat faragjanak.

Az erdőben 8-9 fős csoportokra osztottak minket, és versenyt hirdettek — melyik csoport gyorsabban issza meg egy üveg vodkát, feltéve, hogy a csoport első tagja megtölti a poharat, a második megissza, a harmadik pedig harap egy falatot. Az, aki elvégezte a feladatát, a csoport végére állt.

Az eset, amikor a sor hossza hárommal osztható volt, és jól megvalósította az SRP-t.

1. meghatározás. Egyesített felelősség.

Az egységes felelősség elve (SRP) hivatalos meghatározása szerint minden objektumnak van egy felelőssége és létezésének oka, és ez a felelősség csak egy van.

Nézzük az "Ivó" nevezetű objektumot (Tippler).
Az SRP elvének érdekében osszuk fel a feladatokat hármasával:

  • Az egyik tölt (PourOperation)
  • Az egyik iszik (DrinkUpOperation)
  • Az egyik harap (TakeBiteOperation)

A folyamat minden résztvevője felelős egy folyamat komponenséért, vagyis egy atomikus felelősséggel bír — inni, tölteni vagy harapni.

Az Ivó viszont egy álarcot képez ezek számára az műveletek számára:

class Tippler {
    //...
    void Act(){
        _pourOperation.Do() // tölteni
        _drinkUpOperation.Do() // inni
        _takeBiteOperation.Do() // harapni
    }
}

Egységes Felelősség Elve. Nem annyira egyszerű, mint amilyennek tűnik

Miért?

A programozó ember kódot ír egy majomnak, és a majom figyelmetlen, buta és örökké rohan. Képes egyszerre csak 3-7 kifejezést megérteni és megjegyezni.
Az Ivó esetében ezek a kifejezések hárman vannak. Azonban, ha egy nagy egyszerű kódot írunk, abban megjelennek kezek, poharak, verekedések és végtelen viták a politikáról. Mindez pedig egyetlen metódus testében. Biztos vagyok benne, hogy láttál már ilyen kódot a gyakorlatod során. Nem éppen a legkegyesebb próbája a pszichének.

Másrészt az ember-mókus arra van programozva, hogy a valós világ objektumait modellezze a fejében. A képzeletében összeütköztetheti őket, új objektumokat építhet belőlük, és éppen így szét is bonthatja őket. Képzelj el egy régi autómodelt. A képzeletedben kinyithatod az ajtót, leszedheted az ajtó burkolatát, és láthatod ott az ablakemelő mechanizmusokat, amelyekben fogaskerekek találhatóak. De egyszerre nem láthatod az autó összes alkatrészét, egyetlen "listázásban". Legalábbis az "ember-mókus" nem tudja.

Ezért a programozó emberek lebontják a bonyolult mechanizmusokat egyszerűbb, működő elemekre. Azonban a lebontás módja eltérő lehet: sok régi autóban a levegőjárat az ajtóba vezet, míg a modern autókban a zár elektronikai hibája miatt nem indul el a motor, ami megnehezíti a javítást.

Na szóval, A SRP egy elv, amely megmagyarázza, HOGYAN kell lebontani, azaz hol kell meghúzni a határvonalat..

Azt mondja, hogy a lebontásnak a "felelősség" elve szerint kell történnie, azaz az egyes objektumok feladatai szerint.

Egységes Felelősség Elve. Nem annyira egyszerű, mint amilyennek tűnik

Visszatérve a részeg emberhez és az előnyökre, amelyeket az ember-mókus kap a lebontással:

  • A kód minden szinten rendkívül világossá vált.
  • A kódot több programozó egyszerre írhatja (mindegyik külön elemet ír).
  • Megkönnyíti az automatikus tesztelést - minél egyszerűbb az elem, annál könnyebb tesztelni.
  • Megjelenik a kód kompozícióképessége - cserélhetsz DrinkUpOperation olyan műveletre, amelyben a részeg ember folyadékot önt a asztal alá. Vagy cserélheted az öntést egy olyan műveletre, amelyben kevered a bort és a vizet vagy a vodkát és a sört. Az üzleti igényektől függően mindent megtehetsz anélkül, hogy megérintenéd a metódus kódját. Tippler.Act.
  • Ezekből a műveletekből összeállíthatod a mohót (csak a TakeBitOperation), az alkoholist (csak a DrinkUpOperation közvetlenül az üvegből) és kielégítheted számos más üzleti igényt.

(Ó, úgy tűnik, ez már az OCP elve, és megsértettem a poszt felelősségét.)

És persze, hátrányok is vannak:

  • Több típust kell létrehozni.
  • A részeg ember először pár órával később iszik, mint tehetné.

2. meghatározás. Egységes változékonyság.

Hölgyeim és uraim! A részeg klasszusa is egyetlen felelősséget teljesít — ő iszik! Egyáltalán, a 'felelősség' kifejezés rendkívül homályos fogalom. Valaki felelős az emberiség sorsáért, míg más valaki a sarkon felborult pingvének az emeléséért.

Nézzük meg a részeg két megvalósítását. Az első, amelyet fent említettem, három osztályt tartalmaz — öntés, ivás és falatozás.

A második, a 'Csak előre' metodológián keresztül íródott, és minden logikát az 'Act' metódusban tartalmaz. 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);
    }
   }
}

Ezen osztályok, a külső megfigyelő szempontjából, teljesen egyformán néznek ki, és egyetlen felelősséggel bírnak — 'inni'.

Zavar!

Ekkor elővesszük az internetet, és megtudjuk a SRP másik meghatározását — Az Egyéni Változási Elv (Single Changeability Principle).

Az SCP azt mondja, hogy "A modulnak van egy és egyetlen oka a változásra". Tehát a 'Felelősség — ez a változás oka'.

(Úgy tűnik, azok a srácok, akik az eredeti meghatározást kitalálták, biztosak voltak az ember-majomsor telepatikus képességeiben)

Most minden a helyére kerül. Külön választhatjuk meg a töltési, ivási és falatozási folyamatokat, míg a részegben csak a műveletek sorrendjét és összetételét változtathatjuk meg, például áthelyezve a falatozást ivás előtt vagy hozzáadva a toast felolvasását.

A 'Csak előre' megközelítésben, minden, amit meg lehet változtatni — csakis az 'Act' metodusban változik. ActEz olvasható és hatékony lehet, ha kicsit van logika és ritkán változik, de gyakran rémes 500 soros metódusokhoz vezet, melyekben a beágyazott if-ek száma nagyobb, mint amennyi szükséges Oroszország NATO-ba való belépéséhez.

3. meghatározás. A változások lokalizálása.

A részegek gyakran nem értik, miért ébredtek egy idegen lakásban, vagy hol van a mobiltelefonjuk. Eljött az idő, hogy részletes naplózást adjunk hozzá.

Kezdjük a naplózást a töltési folyamattal:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Mielőtt töltenék {_hand} és {_bottle} használatával");
        //Üzleti logika a töltéshez ...
        _log.Log($"Töltés után {_hand} és {_bottle} használatával");
    }
}

A belefoglalás biztosítja ezt PourOperation, bölcsen jártunk el a felelősség és az inkapszulázás szempontjából, de most a változékonyság elve körüli zűrzavarba kerültünk. Magán a műveleten túl, amely változhat, maga a naplózás is változó lesz. El kell különítenünk, és külön naplózót kell készítenünk a tölteni művelethez:

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{
             //... üzleti logika
             _log.LogAfter(_hand, _bottle);
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

A részletes olvasó észre fogja venni, hogy LogAfter, LogBefore és OnError külön-külön is változhatnak, így az előző lépésekhez hasonlóan három osztályt fog létrehozni: PourLoggerBefore, PourLoggerAfter és PourErrorLogger.

Ha emlékezünk arra, hogy a tölteni műveletek száma három, akkor kilenc naplózó osztályunk lesz. Végül tehát az összes tölteni 14 (!!!) osztályból áll.

Hiperbola? Aligha! Az embermajom egy dekompozíciós gránáttal feldarabolja a "töltőt" kancsóra, pohárra, tölteni operátorokra, vízszolgáltató szolgáltatásokra, molekulák ütközési fizikai modelljére, és a következő negyedévet az összefüggések kibogozásával tölti globális változók nélkül. És higgyék el, nem fog megállni.

Pont ezen a ponton sokan arra a következtetésre jutnak, hogy az SRP csak mese a rózsaszínű királyságokból, és elmennek tésztát gyártani…

… anélkül, hogy tudomást szereznének a harmadik definíció létezéséről:

„Az egyetlen felelősség elve azt mondja, hogy a hasonló változásokkal rendelkező dolgokat egy helyen kell tartani.“Vagyis, amit együtt változtatunk, azt egy helyen kell tartanunk.Azaz, ha a művelet naplózását megváltoztatjuk, azt egy helyen kell módosítanunk.”

Ez nagyon fontos pont – mivel az eddigi SRP magyarázatok arról szóltak, hogy daraboljuk fel a típusokat annyira, amennyire lehetséges, ami „felső korlátot” szabott az objektum méretére, most már a „alsó korlátról” is beszélünk. Más szavakkal,

az SRP nemcsak megkívánja a „darabolást, amíg lehet”, hanem azt is, hogy ne vigyük túlzásba – „ne daraboljuk szét a kapcsolódó dolgokat”. . Ez Ockham borotvájának és az embermajomnak a nagy csatája!Most a tölteni személy számára könnyebbnek kell lennie. Amellett, hogy nem szükséges az IPourLogger naplózót három osztályra bontani, az összes naplózót összegyűjthetjük egy típusba:

Egységes Felelősség Elve. Nem annyira egyszerű, mint amilyennek tűnik

Most a folyamat során könnyebb dolga lesz. Ezen kívül, hogy nem szükséges az IPourLogger naplózót három osztályra darabolni, az összes naplózót egy típusba is össze tudjuk vonni:

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

És ha hozzáadnánk egy negyedik művelettípust, akkor a naplózás már kész van arra. A műveletek kódja tiszta, és mentes az infrastruktúra zajától.

Ennek eredményeként öt osztályunk van a megoldásra: a kiömléshez.

  • Öntési művelet
  • Kiömlési művelet
  • Falatozási művelet
  • Naplózó
  • Borravaló fasád

Mindegyik szigorúan egy funkcióért felelős, és egy okból változtatni kell rajta. Minden hasonló változtatási szabály közel helyezkedik el.

Való élet példa

Egyszer egy b2b ügyfél automatikus regisztrációs szolgáltatást írtunk. És megjelent a GOD-metódus 200 sor hasonló tartalommal:

  • Menj az 1C-be és nyiss egy számlát
  • Ezzel a számlával menj a fizetési modulhoz, és nyiss egyet ott
  • Ellenőrizd, hogy ilyen számlás fiók nincs-e a főben szerveren
  • Hozz létre egy új fiókot
  • A regisztrációs modulban a regisztráció eredményét és az 1C számát add hozzá a regisztrációs eredmények szolgáltatáshoz
  • Add hozzá ezt az információt a számlához
  • Hozz létre egy pont számot ennek az ügyfélnek a pontszolgáltatásban. Add át ennek a szolgáltatásnak az 1C számszámát.

És a listában volt még körülbelül 10 üzleti művelet rettenetes összefonódással. A számlához szükséges objektum szinte mindenki számára lényeges volt. A pontazonosító és az ügyfél neve a hívások felének felében volt szükséges.

Egy órás refaktorálás után sikerült elkülöníteni az infrastruktúra kódot és az ügyféllel kapcsolatos munkát különálló metódusokba/osztályokba. A God metódus könnyebb lett, de maradt 100 sor kód, ami nem akarta magát kibogozni.

Csak néhány nappal később jött a felismerés, hogy ennek a "megkönnyített" metódusnak az esszenciája maga az üzleti algoritmus. És hogy az eredeti követelmény leírás meglehetősen bonyolult volt. Valójában a metódus darabokra törése megsértené az SRP-t, és nem fordítva.

Formalizmus.

Elérkezett az idő, hogy békén hagyjuk a borravalónkat. Töröljétek le a könnyeiteket — vissza fogunk térni hozzá valamikor. Most viszont formáljuk meg a cikkből szerzett tudást.

Formalizmus 1. Az SRP meghatározása

  1. Ossza szét az elemeket úgy, hogy mindegyik csupán egy dologért felelős legyen.
  2. A felelősség a "változtatási okként" van felbontva. Ez azt jelenti, hogy minden egyes elemnek csak egy változtatási oka van, az üzleti logika terminológiájában.
  3. A várható üzleti logikában bekövetkező változások lokalizálva kell, hogy legyenek. A párhuzamosan módosuló elemeknek közel kell lenniük egymáshoz.

Formalizmus 2. Szükséges önellenőrzési kritériumok.

Nem találkoztam elegendő SRP teljesítési kritériummal. De vannak szükséges feltételek:

1) Tegye fel magának a kérdést – mit csinál ez az osztály/metódus/modul/szolgáltatás. Egyszerű meghatározásban kell tudnia válaszolni. (köszönöm Brightori )

magyarázatok

Néha azonban nagyon nehéz egyszerű meghatározást találni.

2) Egy hiba javítása vagy új funkció hozzáadása a lehető legkevesebb fájlt/osztályt érinti. Ideális esetben – egyet.

magyarázatok

Mivel a felelősség (a funkcióért vagy hibáért) egy fájlban/osztályban van kapszulázva, pontosan tudja, hol keresse és mit javítson. Például: a műveletek naplózásának kimenetének megváltoztatása csak a naplózót igényli. Nincs szükség az összes többi kód átnézésére.

Egy másik példa – új UI-vezérlő hozzáadása, amely hasonló a korábbiakhoz. Ha ez azt követeli meg, hogy 10 különböző entitást és 15 különböző konvertert kell hozzáadnia – úgy tűnik, "túlkomplikálta".

3) Ha több fejlesztő dolgozik a projekt különböző funkcióin, akkor a merge konfliktus valószínűsége, vagyis annak a valószínűsége, hogy ugyanazt a fájlt/osztályt több fejlesztő egyszerre módosít, minimális.

magyarázatok

Ha az új "Írjunk vodkát az asztal alá" művelet hozzáadásakor érintenie kell a naplózót, az ivás és öntés műveletet – akkor úgy tűnik, hogy a felelősségek nem jól vannak elosztva. Ez természetesen nem mindig lehetséges, de törekedni kell ennek csökkentésére.

4) Ha tisztázó kérdést tesz fel az üzleti logikáról (fejlesztőtől vagy menedzsertől), akkor szigorúan egy fájlba/osztályba nyúl és csak onnan kap információt.

magyarázatok

A funkciók, szabályok vagy algoritmusok tömören, mindegyik egy helyen vannak megírva, nem pedig a kód tér minden pontján szétszórva.

5) A névadász világos.

magyarázatok

Osztályunk vagy metódusunk felelős valamiért, és a felelősség tükröződik a nevében.

AllManagersManagerService – valószínűleg Isten-osztály.
LocalPayment – valószínűleg nem.

Formalizmus 3. Az "Ockham-first" fejlesztési módszertan.

A tervezés kezdetén a majomember nem ismeri és nem érzi a megoldandó feladat minden árnyalatát, és hibázhat. Sokféleképpen lehet hibázni:

  • Túl nagy objektumokat létrehozni különböző felelősségek összeragasztásával
  • Szétbontani az egyes felelősséget sok különböző típusra
  • Helytelenül meghatározni a felelősségek határait

Fontos megjegyezni a szabályt: „jobb nagyobb mértékben hibázni”, vagy „ha nem biztos a dolgában, ne bontson szét”. Ha például az osztálya két felelősséget gyűjt magába - akkor az még mindig érthető, és azt két részre lehet osztani a klienskód minimális megváltoztatásával. Üvegdarabokból poharat összerakni általában nehezebb a több fájlra elmosódott kontextus és a klienskód szükséges függőségeinek hiánya miatt.

Ideje befejezni

Az SRP alkalmazási köre nem korlátozódik az OOP-ra és a SOLID-ra. Alkalmazható módszerekre, funkciókra, osztályokra, modulokra, mikroszolgáltatásokra és szolgáltatásokra. Jelen van a „hack-hack-be a prod”-ban és a „rakétatudomány” fejlesztésben, mindenhol egy kicsit jobbá téve a világot. Ha jobban belegondolunk, ez talán az egész mérnöki tudomány alapelve. A gépészet, vezérlőrendszerek és szinte minden bonyolult rendszer – alkatrészekből épülnek fel, és a „nem megfelelő szétbontás” megfosztja a tervezőket a rugalmasságtól, a „túlzott szétbontás” pedig a hatékonyságtól, míg a helytelen határok – az értelemtől és a lelki nyugattól.

Egységes Felelősség Elve. Nem annyira egyszerű, mint amilyennek tűnik

Az SRP nem a természettől ered, és nem része a pontos tudománynak. Azokból a biológiai és pszichológiai korlátainkból származik, amik mindannyiunkra jellemzők. Ez csupán egy módszer a bonyolult rendszerek irányítására és fejlesztésére az emberek elméjének segítségével. Megmutatja számunkra, hogyan lehet dekonstrukciót végezni a rendszeren. Az eredeti megfogalmazás jelentős telepátiás készségeket igényelt, de remélem, hogy ez a cikk egy kicsit eloszlatta a füstöt.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster