Principi de Responsabilitat Única. No és tan senzill com sembla

Principi de Responsabilitat Única. No és tan senzill com sembla Principi de responsabilitat única, també conegut com a principi de responsabilitat única,
també conegut com el principi de variabilitat uniforme: un tipus extremadament relliscós per entendre i una pregunta tan nerviosa en una entrevista amb un programador.

El meu primer coneixement seriós d'aquest principi va tenir lloc a principis del primer any, quan els joves i els verds van ser portats al bosc per fer estudiants de larves: autèntics estudiants.

Al bosc, ens vam dividir en grups de 8-9 persones cadascun i vam fer una competició: quin grup beuria una ampolla de vodka més ràpid, sempre que la primera persona del grup aboqués vodka en un got i la segona el begués, i el tercer té un berenar. La unitat que ha completat el seu funcionament es mou al final de la cua del grup.

El cas en què la mida de la cua era múltiple de tres va ser una bona implementació de SRP.

Definició 1. Responsabilitat única.

La definició oficial del Principi de Responsabilitat Única (SRP) estableix que cada entitat té la seva pròpia responsabilitat i raó d'existència, i només té una responsabilitat.

Considereu l'objecte "Bevedor" (Tippler).
Per implementar el principi SRP, dividirem les responsabilitats en tres:

  • Un aboca (Operació per)
  • Un beu (Operació DrinkUp)
  • Un pren un berenar (TakeBiteOperation)

Cadascun dels participants en el procés és responsable d'un component del procés, és a dir, té una responsabilitat atòmica: beure, abocar o berenar.

El beure, al seu torn, és una façana per a aquestes operacions:

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

Principi de Responsabilitat Única. No és tan senzill com sembla

Per què?

El programador humà escriu codi per a l'home-mono, i l'home-mono és poc atent, estúpid i sempre té pressa. Pot contenir i entendre entre 3 i 7 termes alhora.
En el cas d'un borratxo, hi ha tres d'aquests termes. No obstant això, si escrivim el codi amb un full, llavors contindrà mans, ulleres, baralles i interminables discussions sobre política. I tot això estarà en el cos d'un mètode. Estic segur que heu vist aquest codi a la vostra pràctica. No és la prova més humana per a la psique.

D'altra banda, l'home simi està dissenyat per simular objectes del món real al seu cap. En la seva imaginació, pot unir-los, muntar-los nous objectes i desmuntar-los de la mateixa manera. Imagineu-vos un vell model de cotxe. En la vostra imaginació, podeu obrir la porta, desenroscar el revestiment de la porta i veure allà els mecanismes d'elevació de finestres, dins dels quals hi haurà engranatges. Però no podeu veure tots els components de la màquina alhora, en un "llistat". Almenys l'"home mico" no pot.

Per tant, els programadors humans descomponen mecanismes complexos en un conjunt d'elements menys complexos i de treball. Tanmateix, es pot descompondre de diferents maneres: en molts cotxes antics, el conducte d'aire entra a la porta, i en els cotxes moderns, una fallada en l'electrònica del pany impedeix que el motor s'engegui, cosa que pot ser un problema durant les reparacions.

Ara, SRP és un principi que explica COM descompondre, és a dir, on dibuixar la línia divisòria.

Diu que cal descompondre's segons el principi de divisió de la "responsabilitat", és a dir, segons les tasques de determinats objectes.

Principi de Responsabilitat Única. No és tan senzill com sembla

Tornem a beure i als avantatges que rep l'home mico durant la descomposició:

  • El codi s'ha tornat molt clar a tots els nivells
  • El codi pot ser escrit per diversos programadors alhora (cadascun escriu un element independent)
  • Les proves automatitzades es simplifiquen: com més simple sigui l'element, més fàcil serà la prova
  • Apareix la composició del codi: podeu substituir-lo Operació DrinkUp a una operació en què un borratxo aboca líquid sota la taula. O substituïu l'operació d'abocament per una operació en què barregeu vi i aigua o vodka i cervesa. Depenent dels requisits empresarials, podeu fer-ho tot sense tocar el codi del mètode Tippler.Actuar.
  • A partir d'aquestes operacions podeu plegar el glotón (només utilitzant TakeBitOperation), Alcohòlic (només de consum Operació DrinkUp directament de l'ampolla) i compleixen molts altres requisits empresarials.

(Oh, sembla que això ja és un principi OCP, i he violat la responsabilitat d'aquesta publicació)

I, per descomptat, els contres:

  • Haurem de crear més tipus.
  • Un borratxo beu per primera vegada un parell d'hores més tard del que hauria fet d'una altra manera.

Definició 2. Variabilitat unificada.

Permeteu-me, senyors! La classe de begudes també té una única responsabilitat: beu! I en general, la paraula "responsabilitat" és un concepte extremadament vague. Algú és responsable del destí de la humanitat, i algú és responsable de criar els pingüins que es van bolcar al pal.

Considerem dues implementacions del bevedor. El primer, esmentat anteriorment, conté tres classes: abocar, beure i berenar.

El segon està escrit a través de la metodologia "Endavant i només endavant" i conté tota la lògica del mètode Actuar:

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

Ambdues classes, des del punt de vista d'un observador extern, es veuen exactament iguals i comparteixen la mateixa responsabilitat de "beure".

Confusió!

A continuació, ens connectem i descobrim una altra definició de SRP: el principi de canvi únic.

SCP afirma que "Un mòdul té un i només un motiu per canviar". És a dir, "La responsabilitat és un motiu de canvi".

(Sembla que els nois que van plantejar la definició original confiaven en les habilitats telepàtiques de l'home simi)

Ara tot cau al seu lloc. Per separat, podem canviar els procediments d'abocament, beure i berenar, però en el mateix bevedor només podem canviar la seqüència i la composició de les operacions, per exemple, movent el berenar abans de beure o afegint la lectura d'un brindis.

En l'enfocament "Endavant i només endavant", tot el que es pot canviar només es modifica en el mètode Actuar. Això pot ser llegible i eficaç quan hi ha poca lògica i rarament canvia, però sovint acaba en mètodes terribles de 500 línies cadascuna, amb més declaracions si que les necessàries perquè Rússia s'incorpori a l'OTAN.

Definició 3. Localització dels canvis.

Sovint, els bevedors no entenen per què es van despertar a l'apartament d'una altra persona o on és el seu telèfon mòbil. És hora d'afegir un registre detallat.

Comencem a registrar amb el procés d'abocament:

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

En encapsular-lo Operació per, vam actuar sàviament des del punt de vista de la responsabilitat i l'encapsulació, però ara ens confonen amb el principi de variabilitat. A més de l'operació en si, que pot canviar, el registre en si mateix també esdevé modificable. Haureu de separar i crear un registre especial per a l'operació d'abocament:

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

El lector meticulós ho notarà LogAfter, Inicieu sessió abans и OnError també es pot canviar individualment i, per analogia amb els passos anteriors, es crearan tres classes: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

I recordant que hi ha tres operacions per a un bevedor, obtenim nou classes de tala. Com a resultat, tot el cercle de begudes consta de 14 (!!!) classes.

Hipèrbola? Amb prou feines! Un home mico amb una granada de descomposició dividirà el "abocador" en un decantador, un got, operadors d'abocament, un servei de subministrament d'aigua, un model físic de la col·lisió de molècules, i durant el proper trimestre intentarà desenredar les dependències sense variables globals. I creieu-me, no pararà.

És en aquest punt que molts arriben a la conclusió que els SRP són contes de fades dels regnes rosats i se'n van a jugar als fideus...

... sense saber mai l'existència d'una tercera definició de Srp:

“El Principi de Responsabilitat Única ho diu les coses que són semblants al canvi s'han d'emmagatzemar en un sol lloc". o "El que canvia junts s'ha de guardar en un sol lloc"

És a dir, si canviem el registre d'una operació, l'hem de canviar en un sol lloc.

Aquest és un punt molt important, ja que totes les explicacions de SRP anteriors deien que era necessari aixafar els tipus mentre estaven aixafats, és a dir, imposaven un "límit superior" a la mida de l'objecte, i ara ja estem parlant d'un "límit inferior". En altres paraules, SRP no només requereix "aixafar mentre es tritura", sinó també no exagerar: "no aixafeu les coses entrellaçades". Aquesta és la gran batalla entre la navalla d'Occam i l'home simi!

Principi de Responsabilitat Única. No és tan senzill com sembla

Ara el bevedor hauria de sentir-se millor. A més del fet que no cal dividir el registrador IPourLogger en tres classes, també podem combinar tots els registradors en un sol tipus:

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

I si afegim un quart tipus d'operació, el registre ja està llest. I el codi de les operacions en si és net i lliure de soroll de la infraestructura.

Com a resultat, tenim 5 classes per resoldre el problema de la beguda:

  • Operació d'abocament
  • Operació de beguda
  • Operació d'embussos
  • Registrador
  • Façana de bevedor

Cadascun d'ells és estrictament responsable d'una funcionalitat i té un motiu per canviar. Totes les regles semblants al canvi es troben a prop.

Exemple de la vida real

Una vegada vam escriure un servei per registrar automàticament un client b2b. I va aparèixer un mètode GOD per a 200 línies de contingut similar:

  • Aneu a 1C i creeu un compte
  • Amb aquest compte, aneu al mòdul de pagament i creeu-lo allà
  • Comproveu que no s'hagi creat cap compte amb aquest compte al servidor principal
  • Crear un compte nou
  • Afegiu els resultats del registre al mòdul de pagament i el número 1c al servei de resultats del registre
  • Afegiu la informació del compte a aquesta taula
  • Creeu un número de punt per a aquest client al servei de punts. Passeu el vostre número de compte 1c a aquest servei.

I hi havia unes 10 operacions comercials més en aquesta llista amb una connectivitat terrible. Gairebé tothom necessitava l'objecte del compte. L'identificador del punt i el nom del client eren necessaris a la meitat de les trucades.

Després d'una hora de refactorització, vam poder separar el codi d'infraestructura i alguns dels matisos de treballar amb un compte en mètodes/classes separats. El mètode de Déu ho va fer més fàcil, però quedaven 100 línies de codi que simplement no volien desenredar-se.

Només després d'uns dies va quedar clar que l'essència d'aquest mètode "lleuger" és un algorisme empresarial. I que la descripció original de les especificacions tècniques era força complexa. I és l'intent de trencar aquest mètode en trossos el que violarà l'SRP, i no viceversa.

Formalisme.

És hora de deixar en pau el nostre borratxo. Asseca les llàgrimes: segur que hi tornarem algun dia. Ara formalitzem el coneixement d'aquest article.

Formalisme 1. Definició de SRP

  1. Separa els elements de manera que cadascun d'ells sigui responsable d'una cosa.
  2. La responsabilitat significa "raó per canviar". És a dir, cada element només té un motiu de canvi, pel que fa a la lògica empresarial.
  3. Canvis potencials en la lògica empresarial. s'han de localitzar. Els elements que canvien de manera sincrònica han d'estar a prop.

Formalisme 2. Criteris d'autoavaluació necessaris.

No he vist criteris suficients per complir l'SRP. Però hi ha condicions necessàries:

1) Pregunteu-vos què fa aquesta classe/mètode/mòdul/servei. l'has de respondre amb una definició senzilla. ( Gràcies Brightori )

explicacions

Tanmateix, de vegades és molt difícil trobar una definició senzilla

2) Corregir un error o afegir una funció nova afecta un nombre mínim de fitxers/classes. Idealment - un.

explicacions

Com que la responsabilitat (d'una característica o d'un error) està encapsulada en un fitxer/classe, saps exactament on buscar i què editar. Per exemple: la característica de canviar la sortida de les operacions de registre requerirà canviar només el registrador. No cal executar la resta del codi.

Un altre exemple és afegir un nou control d'IU, similar als anteriors. Si això us obliga a afegir 10 entitats diferents i 15 convertidors diferents, sembla que us esteu exagerant.

3) Si diversos desenvolupadors estan treballant en diferents característiques del vostre projecte, aleshores la probabilitat d'un conflicte de combinació, és a dir, la probabilitat que el mateix fitxer/classe sigui canviat per diversos desenvolupadors alhora, és mínima.

explicacions

Si, quan afegiu una nova operació "Aboqueu vodka sota la taula", heu d'afectar el registrador, l'operació de beure i abocar, sembla que les responsabilitats es divideixen de manera torta. Per descomptat, això no sempre és possible, però hem d'intentar reduir aquesta xifra.

4) Quan se li fa una pregunta clarificadora sobre la lògica empresarial (d'un desenvolupador o gestor), entreu estrictament a una classe/fitxer i només rebeu informació d'allà.

explicacions

Les característiques, regles o algorismes s'escriuen de manera compacta, cadascun en un sol lloc i no es troben dispersos amb banderes per tot l'espai del codi.

5) La denominació és clara.

explicacions

La nostra classe o mètode és responsable d'una cosa, i la responsabilitat es reflecteix en el seu nom

AllManagersManagerService - molt probablement una classe de Déu
LocalPayment, probablement no

Formalisme 3. Metodologia de desenvolupament Occam-first.

Al principi del disseny, l'home mico no sap i no sent totes les subtileses del problema que s'està resolent i pot cometre un error. Podeu cometre errors de diferents maneres:

  • Feu que els objectes siguin massa grans combinant diferents responsabilitats
  • Reformulació dividint una única responsabilitat en molts tipus diferents
  • Definir incorrectament els límits de la responsabilitat

És important recordar la regla: "és millor cometre un gran error" o "si no n'estàs segur, no ho dividis". Si, per exemple, la vostra classe conté dues responsabilitats, encara és comprensible i es pot dividir en dos amb canvis mínims al codi del client. Muntar un got a partir de fragments de vidre sol ser més difícil a causa del context que es distribueix entre diversos fitxers i la manca de dependències necessàries en el codi del client.

És hora d'anomenar-ho un dia

L'abast de SRP no es limita a OOP i SOLID. S'aplica a mètodes, funcions, classes, mòduls, microserveis i serveis. S'aplica tant al desenvolupament "figax-figax-and-prod" com al desenvolupament de la "ciència de coets", fent que el món sigui una mica millor a tot arreu. Si ho penseu, aquest és gairebé el principi fonamental de tota l'enginyeria. L'enginyeria mecànica, els sistemes de control i, de fet, tots els sistemes complexos es construeixen a partir de components, i la "subfragmentació" priva els dissenyadors de flexibilitat, la "sobrefragmentació" priva els dissenyadors d'eficiència i els límits incorrectes els privaven de raó i tranquil·litat.

Principi de Responsabilitat Única. No és tan senzill com sembla

SRP no està inventat per naturalesa i no forma part de la ciència exacta. Trenca les nostres limitacions biològiques i psicològiques, és només una manera de controlar i desenvolupar sistemes complexos utilitzant el cervell de l'home-mono. Ens explica com descompondre un sistema. La formulació original requeria una bona quantitat de telepatia, però espero que aquest article aclareixi part de la cortina de fum.

Font: www.habr.com

Afegeix comentari