Principio de Responsabilidade Única. Non tan sinxelo como parece

Principio de Responsabilidade Única. Non tan sinxelo como parece Principio de responsabilidade única, tamén coñecido como principio de responsabilidade única,
tamén coñecido como o principio de variabilidade uniforme: un tipo moi esvaradío de entender e unha pregunta tan nerviosa nunha entrevista de programador.

O meu primeiro coñecemento serio con este principio tivo lugar a principios do primeiro ano, cando os mozos e os verdes foron levados ao bosque para facer estudantes de larvas, verdadeiros estudantes.

No bosque, dividímonos en grupos de 8-9 persoas cada un e tivemos unha competición: que grupo bebería unha botella de vodka máis rápido, sempre que a primeira persoa do grupo verte vodka nun vaso, o segundo beba, e o terceiro merenda. A unidade que completou a súa operación móvese ao final da cola do grupo.

O caso no que o tamaño da cola era múltiplo de tres foi unha boa implementación de SRP.

Definición 1. Responsabilidade única.

A definición oficial do Principio de Responsabilidade Única (SRP) establece que cada entidade ten a súa propia responsabilidade e razón de existencia, e só ten unha responsabilidade.

Considere o obxecto "Bebedor" (Tippler).
Para implementar o principio SRP, dividiremos as responsabilidades en tres:

  • Un verte (Operación de vertedura)
  • Un bebe (Operación DrinkUp)
  • Un merenda (TakeBiteOperation)

Cada un dos participantes no proceso é responsable dun compoñente do proceso, é dicir, ten unha responsabilidade atómica: beber, verter ou merenda.

O bebedoiro, á súa vez, é unha fachada para estas operacións:

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

Principio de Responsabilidade Única. Non tan sinxelo como parece

Por que?

O programador humano escribe código para o home-mono, e o home-mono é desatento, estúpido e sempre ten présa. Pode manter e comprender entre 3 e 7 termos á vez.
No caso dun borracho, hai tres destes termos. Non obstante, se escribimos o código cunha soa folla, entón conterá mans, lentes, pelexas e interminables discusións sobre política. E todo isto estará no corpo dun método. Estou seguro de que xa viches ese código na túa práctica. Non é a proba máis humana para a psique.

Por outra banda, o home mono está deseñado para simular obxectos do mundo real na súa cabeza. Na súa imaxinación, pode xuntalos, montar novos obxectos a partir deles e desmontalos do mesmo xeito. Imaxina un modelo de coche vello. Na túa imaxinación, podes abrir a porta, desenroscar o revestimento da porta e ver alí os mecanismos de elevación da fiestra, dentro dos cales haberá engrenaxes. Pero non podes ver todos os compoñentes da máquina ao mesmo tempo, nunha "lista". Polo menos o "home mono" non pode.

Polo tanto, os programadores humanos descompoñen mecanismos complexos nun conxunto de elementos menos complexos e de traballo. Non obstante, pódese descompoñer de diferentes xeitos: en moitos coches antigos, o conduto de aire entra na porta e, nos coches modernos, un fallo na electrónica da pechadura impide que o motor se arranque, o que pode ser un problema durante as reparacións.

Así, SRP é un principio que explica COMO descompoñer, é dicir, onde trazar a liña divisoria.

Di que é necesario descompoñerse segundo o principio de división da "responsabilidade", é dicir, segundo as tarefas de determinados obxectos.

Principio de Responsabilidade Única. Non tan sinxelo como parece

Volvemos á bebida e ás vantaxes que recibe o home mono durante a descomposición:

  • O código quedou moi claro en todos os niveis
  • O código pode ser escrito por varios programadores á vez (cada un escribe un elemento separado)
  • As probas automatizadas simplifícanse: canto máis sinxelo sexa o elemento, máis fácil será probalo
  • Aparece a composicionalidade do código: pode substituír Operación DrinkUp a unha operación na que un borracho bota líquido debaixo da mesa. Ou substituír a operación de vertedura por unha operación na que mesturas viño e auga ou vodka e cervexa. Dependendo dos requisitos empresariais, podes facer todo sen tocar o código do método Tippler.Act.
  • A partir destas operacións podes dobrar o glotón (usando só TakeBitOperation), Alcohólico (só consumir Operación DrinkUp directamente da botella) e cumpre moitos outros requisitos comerciais.

(Oh, parece que este xa é un principio OCP, e violei a responsabilidade desta publicación)

E, por suposto, os contras:

  • Teremos que crear máis tipos.
  • Un borracho bebe por primeira vez un par de horas máis tarde do que tería doutro xeito.

Definición 2. Variabilidade unificada.

Permítanme, señores! A clase de bebidas tamén ten unha única responsabilidade: bebe! E, en xeral, a palabra "responsabilidade" é un concepto moi vago. Alguén é responsable do destino da humanidade e alguén é responsable de criar os pingüíns que foron envorcados no polo.

Consideremos dúas implementacións do bebedor. O primeiro, mencionado anteriormente, contén tres clases: verter, beber e merenda.

O segundo está escrito a través da metodoloxía "Forward and Only Forward" e contén toda a lóxica do método 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);
    }
   }
}

Ambas clases, desde o punto de vista dun observador externo, parecen exactamente iguais e comparten a mesma responsabilidade de "beber".

Confusión!

Despois entramos en liña e descubrimos outra definición de SRP: o Principio único de cambiabilidade.

SCP afirma que "Un módulo ten un e só un motivo para cambiar". É dicir, "A responsabilidade é un motivo de cambio".

(Parece que os mozos que elaboraron a definición orixinal confiaban nas habilidades telepáticas do home mono)

Agora todo cae no seu lugar. Por separado, podemos cambiar os procedementos de vertedura, bebida e merenda, pero no propio bebedor só podemos cambiar a secuencia e composición das operacións, por exemplo, movendo a merenda antes de beber ou engadindo a lectura dun brindis.

No enfoque "Forward and Only Forward", todo o que se pode cambiar só se cambia no método Actuar. Isto pode ser lexible e efectivo cando hai pouca lóxica e raramente cambia, pero moitas veces acaba en métodos terribles de 500 liñas cada un, con máis declaracións if das necesarias para que Rusia se una á OTAN.

Definición 3. Localización de cambios.

Os bebedores moitas veces non entenden por que espertaron no apartamento doutra persoa ou onde está o seu teléfono móbil. É hora de engadir un rexistro detallado.

Comecemos a rexistrar co proceso de vertedura:

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

Encapsulándoo Operación de vertedura, actuamos sabiamente dende o punto de vista da responsabilidade e do encapsulamento, pero agora confúndese co principio de variabilidade. Ademais da propia operación, que pode cambiar, o propio rexistro tamén se fai modificable. Terás que separar e crear un rexistrador especial para a operación de vertedura:

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

O lector meticuloso notarao LogAfter, LogBefore и OnError tamén se pode cambiar individualmente e, por analoxía cos pasos anteriores, crearase tres clases: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

E lembrando que hai tres operacións para un bebedor, temos nove clases de tala. Como resultado, todo o círculo de bebidas consta de 14 (!!!) clases.

¿Hipérbola? Dificilmente! Un home mono cunha granada de descomposición dividirá o "vertidor" nun decantador, un vaso, operadores de vertedura, un servizo de abastecemento de auga, un modelo físico de colisión de moléculas, e durante o próximo trimestre tentará desenredar as dependencias sen variables globais. E créame, non parará.

É neste momento cando moitos chegan á conclusión de que os SRP son contos de fadas dos reinos rosas, e van xogar aos fideos...

... sen coñecer nunca a existencia dunha terceira definición de Srp:

“O Principio de Responsabilidade Única establece que as cousas que son similares ao cambio deben almacenarse nun só lugar". ou "O que cambia xuntos debe manterse nun só lugar"

É dicir, se cambiamos o rexistro dunha operación, entón debemos cambiala nun só lugar.

Este é un punto moi importante, xa que todas as explicacións de SRP anteriores dicían que era necesario esmagar os tipos mentres se esmagaban, é dicir, impuxeron un "límite superior" ao tamaño do obxecto, e agora xa estamos a falar dun “límite inferior” . Noutras palabras, SRP non só require "esmagar mentres se esmaga", senón tamén non esaxere: "non esmague as cousas entrelazadas". Esta é a gran batalla entre a navalla de Occam e o home mono!

Principio de Responsabilidade Única. Non tan sinxelo como parece

Agora o bebedor debería sentirse mellor. Ademais de que non é necesario dividir o rexistrador IPourLogger en tres clases, tamén podemos combinar todos os rexistradores nun só tipo:

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

E se engadimos un cuarto tipo de operación, entón o rexistro para ela xa está listo. E o código das operacións en si está limpo e libre de ruído da infraestrutura.

Como resultado, temos 5 clases para resolver o problema da bebida:

  • Operación de vertedura
  • Operación de beber
  • Operación de atasco
  • Logger
  • Fachada de bebedor

Cada un deles é responsable estritamente dunha funcionalidade e ten un motivo para cambiar. Todas as regras similares ao cambio están nas proximidades.

Exemplo da vida real

Unha vez escribimos un servizo para rexistrar automaticamente un cliente b2b. E apareceu un método GOD para 200 liñas de contido semellante:

  • Vai a 1C e crea unha conta
  • Con esta conta, vai ao módulo de pago e créao alí
  • Comprobe que non se creou unha conta con tal conta no servidor principal
  • Crea unha conta nova
  • Engade os resultados do rexistro no módulo de pago e o número 1c ao servizo de resultados do rexistro
  • Engade información da conta a esta táboa
  • Crea un número de punto para este cliente no servizo de puntos. Pasa o teu número de conta 1c a este servizo.

E había preto de 10 operacións comerciais máis nesta lista cunha conectividade terrible. Case todos necesitaban o obxecto da conta. O ID do punto e o nome do cliente foron necesarios na metade das chamadas.

Despois dunha hora de refactorización, puidemos separar o código de infraestrutura e algúns dos matices de traballar cunha conta en métodos/clases separados. O método de Deus fíxoo máis fácil, pero quedaban 100 liñas de código que simplemente non querían ser desenredadas.

Só despois duns días quedou claro que a esencia deste método "lixeiro" é un algoritmo empresarial. E que a descrición orixinal das especificacións técnicas era bastante complexa. E é o intento de romper este método en anacos o que violará o SRP, e non viceversa.

Formalismo.

É hora de deixar só o noso borracho. Seca as túas bágoas: definitivamente volveremos a el algún día. Agora imos formalizar o coñecemento deste artigo.

Formalismo 1. Definición de SRP

  1. Separa os elementos para que cada un deles sexa responsable dunha cousa.
  2. A responsabilidade significa "razón para cambiar". É dicir, cada elemento só ten un motivo de cambio, en termos de lóxica empresarial.
  3. Posibles cambios na lóxica empresarial. debe ser localizado. Os elementos que cambian de forma sincronizada deben estar preto.

Formalismo 2. Criterios de autotest necesarios.

Non vin criterios suficientes para cumprir o SRP. Pero hai condicións necesarias:

1) Pregúntate que fai esta clase/método/módulo/servizo. debes contestalo cunha definición sinxela. ( Grazas Brightori )

explicacións

Non obstante, ás veces é moi difícil atopar unha definición sinxela

2) Corrixir un erro ou engadir unha nova función afecta a un número mínimo de ficheiros/clases. Idealmente - un.

explicacións

Dado que a responsabilidade (por unha función ou erro) está encapsulada nun ficheiro/clase, sabe exactamente onde buscar e que editar. Por exemplo: a función de cambiar a saída das operacións de rexistro requirirá cambiar só o rexistrador. Non é necesario percorrer o resto do código.

Outro exemplo é engadir un novo control de IU, semellante aos anteriores. Se isto obriga a engadir 10 entidades diferentes e 15 conversores diferentes, parece que estás esaxerando.

3) Se varios desenvolvedores están a traballar en funcións diferentes do seu proxecto, entón a probabilidade dun conflito de combinación, é dicir, a probabilidade de que varios desenvolvedores cambien ao mesmo tempo o mesmo ficheiro/clase é mínima.

explicacións

Se, ao engadir unha nova operación "Verter vodka debaixo da mesa", cómpre afectar ao rexistrador, a operación de beber e verter, entón parece que as responsabilidades están divididas de forma torcida. Por suposto, isto non sempre é posible, pero debemos tentar reducir esta cifra.

4) Cando se lle fai unha pregunta aclaratoria sobre a lóxica empresarial (dun desenvolvedor ou xestor), entra estrictamente a unha clase/ficheiro e só recibe información desde alí.

explicacións

As características, as regras ou os algoritmos están escritos de forma compacta, cada un nun só lugar, e non están espallados con bandeiras polo espazo do código.

5) A denominación é clara.

explicacións

A nosa clase ou método é responsable dunha cousa, e a responsabilidade reflíctese no seu nome

AllManagersManagerService - moi probablemente unha clase de Deus
LocalPayment - probablemente non

Formalismo 3. Metodoloxía de desenvolvemento Occam-first.

Ao comezo do deseño, o home mono non sabe e non sente todas as sutilezas do problema que se está a resolver e pode cometer un erro. Podes cometer erros de diferentes xeitos:

  • Fai que os obxectos sexan demasiado grandes combinando diferentes responsabilidades
  • Reformulación dividindo unha única responsabilidade en moitos tipos diferentes
  • Definir incorrectamente os límites da responsabilidade

É importante lembrar a regra: "é mellor cometer un gran erro" ou "se non estás seguro, non o dividas". Se, por exemplo, a túa clase contén dúas responsabilidades, aínda é comprensible e pódese dividir en dúas con cambios mínimos no código do cliente. Ensamblar un vaso a partir de fragmentos de vidro adoita ser máis difícil debido ao contexto que se estende por varios ficheiros e á falta de dependencias necesarias no código do cliente.

É hora de chamalo día

O alcance do SRP non se limita a OOP e SOLID. Aplícase a métodos, funcións, clases, módulos, microservizos e servizos. Aplícase tanto ao desenvolvemento "figax-figax-and-prod" como ao desenvolvemento da "ciencia do foguete", facendo o mundo un pouco mellor en todas partes. Se pensas niso, este é case o principio fundamental de toda a enxeñaría. A enxeñaría mecánica, os sistemas de control e, de feito, todos os sistemas complexos están construídos a partir de compoñentes, e a "subfragmentación" priva aos deseñadores de flexibilidade, a "sobrefragmentación" priva aos deseñadores de eficiencia e os límites incorrectos privan de razón e tranquilidade.

Principio de Responsabilidade Única. Non tan sinxelo como parece

SRP non é inventado pola natureza e non forma parte da ciencia exacta. Rompe coas nosas limitacións biolóxicas e psicolóxicas, é só unha forma de controlar e desenvolver sistemas complexos utilizando o cerebro do home-mono. El dinos como descompoñer un sistema. A formulación orixinal requiriu unha boa cantidade de telepatía, pero espero que este artigo limpa parte da cortina de fume.

Fonte: www.habr.com

Engadir un comentario