Princípio da Responsabilidade Única. Não é tão simples quanto parece

Princípio da Responsabilidade Única. Não é tão simples quanto parece Princípio da responsabilidade única, também conhecido como princípio da responsabilidade única,
também conhecido como princípio da variabilidade uniforme - um cara extremamente escorregadio de entender e uma pergunta tão nervosa em uma entrevista com um programador.

Meu primeiro contato sério com esse princípio aconteceu no início do primeiro ano, quando os jovens e verdes foram levados para a floresta para transformar as larvas em alunos - verdadeiros alunos.

Na floresta, fomos divididos em grupos de 8 a 9 pessoas cada e fizemos uma competição - qual grupo beberia uma garrafa de vodca mais rápido, desde que a primeira pessoa do grupo colocasse vodca em um copo, a segunda bebesse, e o terceiro faz um lanche. A unidade que concluiu sua operação passa para o final da fila do grupo.

O caso em que o tamanho da fila era múltiplo de três foi uma boa implementação do SRP.

Definição 1. Responsabilidade única.

A definição oficial do Princípio da Responsabilidade Única (SRP) afirma que cada entidade tem a sua própria responsabilidade e razão de existência, e tem apenas uma responsabilidade.

Considere o objeto “Bebedor” (beberrão).
Para implementar o princípio SRP, dividiremos as responsabilidades em três:

  • Um derrama (DespejeOperação)
  • Um bebe (Operação DrinkUp)
  • Um faz um lanche (Operação TakeBite)

Cada um dos participantes do processo é responsável por um componente do processo, ou seja, tem uma responsabilidade atômica - beber, servir ou lanchar.

O bebedouro, por sua vez, é uma fachada para estas operações:

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

Princípio da Responsabilidade Única. Não é tão simples quanto parece

Por quê?

O programador humano escreve código para o homem-macaco, e o homem-macaco é desatento, estúpido e está sempre com pressa. Ele pode reter e compreender cerca de 3 a 7 termos ao mesmo tempo.
No caso de um bêbado, existem três desses termos. Porém, se escrevermos o código em uma folha, ele conterá mãos, óculos, brigas e discussões intermináveis ​​​​sobre política. E tudo isso estará no corpo de um método. Tenho certeza que você já viu esse código em sua prática. Não é o teste mais humano para a psique.

Por outro lado, o homem macaco foi projetado para simular objetos do mundo real em sua cabeça. Em sua imaginação, ele pode juntá-los, montar novos objetos a partir deles e desmontá-los da mesma maneira. Imagine um modelo de carro antigo. Na sua imaginação, você pode abrir a porta, desparafusar o revestimento da porta e ver ali os mecanismos de elevação das janelas, dentro dos quais haverá engrenagens. Mas não dá para ver todos os componentes da máquina ao mesmo tempo, em uma “listagem”. Pelo menos o “homem macaco” não pode.

Portanto, os programadores humanos decompõem mecanismos complexos em um conjunto de elementos menos complexos e funcionais. Porém, pode ser decomposto de diferentes maneiras: em muitos carros antigos, o duto de ar entra pela porta, e nos carros modernos, uma falha na eletrônica da fechadura impede a partida do motor, o que pode ser um problema durante os reparos.

E assim, SRP é um princípio que explica COMO se decompor, ou seja, onde traçar a linha divisória.

Ele diz que é preciso decompor segundo o princípio da divisão da “responsabilidade”, ou seja, de acordo com as tarefas de determinados objetos.

Princípio da Responsabilidade Única. Não é tão simples quanto parece

Voltemos à bebida e às vantagens que o homem macaco recebe durante a decomposição:

  • O código tornou-se extremamente claro em todos os níveis
  • O código pode ser escrito por vários programadores ao mesmo tempo (cada um escreve um elemento separado)
  • O teste automatizado é simplificado – quanto mais simples o elemento, mais fácil é testar
  • A composicionalidade do código aparece - você pode substituir Operação DrinkUp a uma operação em que um bêbado derrama líquido debaixo da mesa. Ou substitua a operação de vazamento por uma operação em que você mistura vinho e água ou vodca e cerveja. Dependendo dos requisitos do negócio, você pode fazer tudo sem mexer no código do método Bebedouro.Agir.
  • A partir destas operações você pode dobrar o glutão (usando apenas Operação TakeBit), Alcoólico (usando apenas Operação DrinkUp direto da garrafa) e atendem a muitos outros requisitos de negócios.

(Ah, parece que isso já é um princípio do OCP, e violei a responsabilidade deste post)

E, claro, os contras:

  • Teremos que criar mais tipos.
  • Um bêbado bebe pela primeira vez algumas horas depois do que normalmente beberia.

Definição 2. Variabilidade unificada.

Permitam-me, senhores! A classe que bebe também tem uma única responsabilidade – ela bebe! E, em geral, a palavra “responsabilidade” é um conceito extremamente vago. Alguém é responsável pelo destino da humanidade e alguém é responsável por criar os pinguins que foram tombados no pólo.

Consideremos duas implementações do bebedor. A primeira, citada acima, contém três classes – servir, beber e lanche.

O segundo é escrito através da metodologia “Forward and Only Forward” e contém toda a lógica do método Aja:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
с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 as classes, do ponto de vista de um observador externo, parecem exatamente iguais e compartilham a mesma responsabilidade de “beber”.

Confusão!

Em seguida, acessamos a Internet e descobrimos outra definição de SRP - o Princípio Único de Mutabilidade.

SCP afirma que “Um módulo tem um e apenas um motivo para mudar". Ou seja, “A responsabilidade é uma razão para a mudança”.

(Parece que os caras que criaram a definição original estavam confiantes nas habilidades telepáticas do homem-macaco)

Agora tudo se encaixa. Separadamente, podemos alterar os procedimentos de servir, beber e petiscar, mas no próprio bebedouro só podemos alterar a sequência e composição das operações, por exemplo, movimentando o lanche antes de beber ou acrescentando a leitura de um brinde.

Na abordagem “Forward and Only Forward”, tudo o que pode ser alterado é alterado apenas no método Aja. Isto pode ser legível e eficaz quando há pouca lógica e raramente muda, mas muitas vezes termina em métodos terríveis de 500 linhas cada, com mais declarações se do que o necessário para a Rússia aderir à NATO.

Definição 3. Localização de mudanças.

Muitas vezes, os bebedores não entendem por que acordaram no apartamento de outra pessoa ou onde está seu celular. É hora de adicionar registros detalhados.

Vamos começar a registrar com o processo de vazamento:

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

Ao encapsula-lo em DespejeOperação, agimos com sabedoria do ponto de vista da responsabilidade e do encapsulamento, mas agora nos confundimos com o princípio da variabilidade. Além da operação em si, que pode mudar, o próprio registro também se torna mutável. Você terá que separar e criar um registrador especial para a operação de vazamento:

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 leitor meticuloso notará que LogDepois, Log antes и AoErro também pode ser alterado individualmente e, por analogia com os passos anteriores, criará três classes: PourLoggerBefore, DespejeLoggerDepois и DespejeErrorLogger.

E lembrando que são três operações para um bebedor, temos nove classes de registro. Como resultado, toda a roda de bebida consiste em 14 (!!!) turmas.

Hipérbole? Dificilmente! Um homem macaco com uma granada de decomposição dividirá o “derramador” em uma garrafa, um copo, operadores de vazamento, um serviço de abastecimento de água, um modelo físico de colisão de moléculas, e no próximo trimestre tentará desembaraçar as dependências sem variáveis ​​globais. E acredite, ele não vai parar.

É neste ponto que muitos chegam à conclusão de que os SRP são contos de fadas de reinos cor de rosa, e vão embora para brincar de macarrão...

... sem nunca saber da existência de uma terceira definição de Srp:

“O Princípio da Responsabilidade Única afirma que coisas que são semelhantes à mudança devem ser armazenadas em um só lugar". ou "O que muda junto deve ser mantido em um só lugar"

Ou seja, se alterarmos o registro de uma operação, devemos alterá-lo em um só lugar.

Esse é um ponto muito importante - já que todas as explicações do SRP que foram acima diziam que era necessário triturar os tipos enquanto eles estavam sendo triturados, ou seja, impuseram um “limite superior” ao tamanho do objeto, e agora já estamos falando de um “limite inferior”. Em outras palavras, O SRP não requer apenas “esmagar enquanto esmaga”, mas também não exagerar - “não esmague coisas interligadas”. Esta é a grande batalha entre a navalha de Occam e o homem macaco!

Princípio da Responsabilidade Única. Não é tão simples quanto parece

Agora quem bebe deve se sentir melhor. Além do fato de não haver necessidade de dividir o registrador IPourLogger em três classes, também podemos combinar todos os registradores em um 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 adicionarmos um quarto tipo de operação, o registro dela já estará pronto. E o código das próprias operações é limpo e livre de ruídos de infraestrutura.

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

  • Operação de vazamento
  • Operação de bebida
  • Operação de bloqueio
  • Registrador
  • Fachada de bebedouro

Cada um deles é responsável estritamente por uma funcionalidade e tem um motivo para alteração. Todas as regras semelhantes à mudança estão localizadas nas proximidades.

Exemplo da vida real

Certa vez, escrevemos um serviço para registrar automaticamente um cliente b2b. E um método GOD apareceu para 200 linhas de conteúdo semelhante:

  • Vá para 1C e crie uma conta
  • Com esta conta, acesse o módulo de pagamento e crie-a lá
  • Verifique se uma conta com essa conta não foi criada no servidor principal
  • Criar uma nova conta
  • Adicione os resultados do registro no módulo de pagamento e o número 1c ao serviço de resultados do registro
  • Adicione informações da conta a esta tabela
  • Crie um número de ponto para este cliente no serviço de ponto. Passe o número da sua conta 1c para este serviço.

E havia mais cerca de 10 operações comerciais nesta lista com conectividade péssima. Quase todo mundo precisava do objeto conta. O ID do ponto e o nome do cliente foram necessários em metade das chamadas.

Após uma hora de refatoração, conseguimos separar o código da infraestrutura e algumas das nuances de trabalhar com uma conta em métodos/classes separados. O método Deus tornou tudo mais fácil, mas restavam 100 linhas de código que simplesmente não queriam ser desembaraçadas.

Só depois de alguns dias ficou claro que a essência desse método “leve” é um algoritmo de negócios. E que a descrição original das especificações técnicas era bastante complexa. E é a tentativa de quebrar esse método em pedaços que violará o SRP, e não vice-versa.

Formalismo.

É hora de deixar nosso bêbado em paz. Seque suas lágrimas - com certeza voltaremos a isso algum dia. Agora vamos formalizar o conhecimento deste artigo.

Formalismo 1. Definição de SRP

  1. Separe os elementos para que cada um deles seja responsável por uma coisa.
  2. Responsabilidade significa “razão para mudar”. Ou seja, cada elemento tem apenas um motivo de mudança, em termos de lógica de negócio.
  3. Possíveis mudanças na lógica de negócios. deve ser localizado. Os elementos que mudam de forma síncrona devem estar próximos.

Formalismo 2. Critérios de autoteste necessários.

Não vi critérios suficientes para cumprir o SRP. Mas existem condições necessárias:

1) Pergunte a si mesmo o que esta classe/método/módulo/serviço faz. você deve responder com uma definição simples. ( Obrigado Brightori )

explicações

No entanto, às vezes é muito difícil encontrar uma definição simples

2) Corrigir um bug ou adicionar um novo recurso afeta um número mínimo de arquivos/classes. Idealmente - um.

explicações

Como a responsabilidade (por um recurso ou bug) está encapsulada em um arquivo/classe, você sabe exatamente onde procurar e o que editar. Por exemplo: o recurso de alterar a saída das operações de registro exigirá a alteração apenas do registrador. Não há necessidade de percorrer o resto do código.

Outro exemplo é adicionar um novo controle de UI, semelhante aos anteriores. Se isso forçar você a adicionar 10 entidades diferentes e 15 conversores diferentes, parece que você está exagerando.

3) Se vários desenvolvedores estiverem trabalhando em recursos diferentes do seu projeto, então a probabilidade de um conflito de mesclagem, ou seja, a probabilidade de o mesmo arquivo/classe ser alterado por vários desenvolvedores ao mesmo tempo, é mínima.

explicações

Se, ao adicionar uma nova operação “Despejar vodka embaixo da mesa”, você precisar afetar o registrador, a operação de beber e servir, então parece que as responsabilidades estão divididas de maneira torta. É claro que isto nem sempre é possível, mas devemos tentar reduzir este número.

4) Quando você faz uma pergunta esclarecedora sobre lógica de negócios (de um desenvolvedor ou gerente), você acessa estritamente uma classe/arquivo e recebe informações apenas de lá.

explicações

Recursos, regras ou algoritmos são escritos de forma compacta, cada um em um só lugar, e não espalhados com sinalizadores por todo o espaço de código.

5) A nomenclatura é clara.

explicações

Nossa classe ou método é responsável por uma coisa, e a responsabilidade está refletida em seu nome

AllManagersManagerService – provavelmente uma classe de Deus
LocalPayment - provavelmente não

Formalismo 3. Metodologia de desenvolvimento Occam-first.

No início do projeto, o homem macaco não conhece e não sente todas as sutilezas do problema que está sendo resolvido e pode cometer erros. Você pode cometer erros de diferentes maneiras:

  • Torne os objetos muito grandes mesclando diferentes responsabilidades
  • Reenquadramento dividindo uma única responsabilidade em muitos tipos diferentes
  • Definir incorretamente os limites de responsabilidade

É importante lembrar a regra: “é melhor cometer um grande erro” ou “se não tiver certeza, não divida”. Se, por exemplo, sua classe contiver duas responsabilidades, ela ainda será compreensível e poderá ser dividida em duas com alterações mínimas no código do cliente. Montar um vidro a partir de cacos de vidro geralmente é mais difícil devido ao contexto estar espalhado por vários arquivos e à falta de dependências necessárias no código do cliente.

É hora de encerrar o dia

O escopo do SRP não se limita a OOP e SOLID. Aplica-se a métodos, funções, classes, módulos, microsserviços e serviços. Aplica-se tanto ao desenvolvimento “figax-figax-and-prod” como ao desenvolvimento da “ciência de foguetes”, tornando o mundo um pouco melhor em todos os lugares. Se você pensar bem, este é quase o princípio fundamental de toda engenharia. A engenharia mecânica, os sistemas de controlo e, na verdade, todos os sistemas complexos são construídos a partir de componentes, e a “subfragmentação” priva os projetistas de flexibilidade, a “sobrefragmentação” priva os projetistas de eficiência e os limites incorretos privam-nos da razão e da paz de espírito.

Princípio da Responsabilidade Única. Não é tão simples quanto parece

O SRP não é inventado pela natureza e não faz parte da ciência exata. Ela rompe com nossas limitações biológicas e psicológicas e é apenas uma forma de controlar e desenvolver sistemas complexos usando o cérebro do homem-macaco. Ele nos diz como decompor um sistema. A formulação original exigia uma boa dose de telepatia, mas espero que este artigo elimine um pouco da cortina de fumaça.

Fonte: habr.com

Adicionar um comentário