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 única responsabilidade,
também conhecido como princípio da única mutabilidade — é um conceito bastante escorregadio para entender e uma questão tão nervosa em entrevistas para programadores.

O meu primeiro contato sério com esse princípio ocorreu no início do primeiro ano da faculdade, quando nós, jovens e inexperientes, fomos levados para a floresta para transformar calouros em estudantes de verdade.

Na floresta, fomos divididos em grupos de 8 a 9 pessoas cada e realizamos uma competição — qual grupo consegue beber uma garrafa de vodka mais rápido, sob a condição de que a primeira pessoa do grupo serve a vodka em um copo, a segunda bebe, e a terceira come algo. O participante que completar sua tarefa vai para o final da fila do grupo.

Um caso em que o tamanho da fila era múltiplo de três se mostrava 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 objeto possui sua responsabilidade e razão de existir, e essa responsabilidade é única.

Vamos considerar o objeto “Apreciador de bebidas” (Tippler).
Para seguir o princípio SRP, vamos dividir as responsabilidades entre três:

  • Um serve (PourOperation)
  • Um bebe (DrinkUpOperation)
  • Um come (TakeBiteOperation)

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

O Apreciador de bebidas, por sua vez, é um fachada para essas operações:

class Tippler {
    //...
    void Act(){
        _pourOperation.Do() // serve
        _drinkUpOperation.Do() // beber
        _takeBiteOperation.Do() // comer
    }
}

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

Por quê?

O programador escreve código para um macaco, e o macaco é desatento, burro e está sempre com pressa. Ele pode compreender de 3 a 7 termos ao mesmo tempo.
No caso do apreciador de bebidas, esses três termos estão presentes. No entanto, se escrevermos tudo em um único bloco de código, aparecerão braços, copos, brigas e discussões intermináveis sobre política. E tudo isso ficará dentro de um único método. Tenho certeza de que você já viu esse tipo de código na sua prática. Não é um teste muito humanitário para a psique.

Por outro lado, o homem-macaco está programado para modelar objetos do mundo real em sua mente. Em sua imaginação, ele pode colidir, juntar novos objetos e desmontá-los da mesma forma. Imagine um modelo antigo de carro. Você pode imaginar abrir a porta, retirar o revestimento da porta e ver os mecanismos dos elevadores de vidro, dentro dos quais estarão as engrenagens. Mas você não pode ver todos os componentes do carro 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. No entanto, a decomposição pode ser feita de diferentes maneiras: em muitos carros antigos - o duto de ar sai pela porta, enquanto nos modernos - uma falha na eletrônica da fechadura impede que o motor arranque, o que traz dificuldades na hora do conserto.

Então, SRP é um princípio que explica COMO decompôr, ou seja, onde traçar a linha de separação..

Ele afirma que a decomposição deve ser feita segundo o princípio de 'separação de responsabilidades', ou seja, com base nas tarefas de determinados objetos.

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

Vamos voltar ao bêbado e aos benefícios que o homem-macaco obtém ao decompôr:

  • O código se torna extremamente claro em cada nível.
  • O código pode ser escrito por vários programadores ao mesmo tempo (cada um escreve um elemento separado).
  • Facilita a automação de testes — quanto mais simples o elemento, mais fácil é testá-lo.
  • Surge a composicionalidade do código — é possível substituir DrinkUpOperation por uma operação na qual o bêbado derrama líquido sob a mesa. Ou substituir a operação de derramar por uma operação onde você mistura vinho e água ou vodka e cerveja. Dependendo dos requisitos de negócios, você pode fazer tudo isso, sem tocar no código do método. Tippler.Act.
  • A partir dessas operações, você pode compor um glutão (usando apenas TakeBitOperation), um alcoólatra (usando apenas DrinkUpOperation diretamente da garrafa) e atender a muitas outras demandas de negócios.

(Oops, parece que isso já é o princípio OCP, e eu violarei a responsabilidade deste post.)

E, claro, os contras:

  • Será necessário criar mais tipos.
  • O bêbado beberá pela primeira vez algumas horas depois do que poderia.

Definição 2. Mudança única.

Senhores, permitam-me! A classe do bebedor também desempenha uma única responsabilidade — ela bebe! E, de fato, a palavra "responsabilidade" é um conceito bastante vago. Alguém é responsável pelo destino da humanidade, enquanto outro é responsável por levantar os pinguins caídos no pólo.

Vamos considerar duas implementações do bebedor. A primeira, mencionada acima, contém três classes — servir, beber e petiscar.

A segunda, escrita sob a metodologia "Avante e somente avante", contém toda a lógica no método 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);
    }
   }
}

Ambas essas classes, do ponto de vista de um observador externo, parecem absolutamente idênticas e têm a mesma responsabilidade de "beber".

Confusão!

Então, vamos à internet e descobrimos outra definição do SRP — Princípio da Responsabilidade Única (Single Responsibility Principle).

O SCP afirma que "Um módulo tem uma e apenas uma razão para mudar". Ou seja, "Responsabilidade é a razão para a mudança".

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

Agora tudo se encaixa. Podemos alterar separadamente os procedimentos de servir, beber e petiscar, e no próprio bebedor podemos mudar apenas a sequência e a composição das operações, por exemplo, movendo o petisco antes de beber ou adicionando a leitura de um brinde.

Na abordagem "Avante e somente avante", tudo o que pode ser mudado — muda apenas no método Act. Isso pode ser legível e eficiente quando a lógica é pouca e muda raramente, mas muitas vezes acaba resultando em métodos horríveis de 500 linhas cada, com mais ifs do que o necessário para a entrada da Rússia na OTAN.

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

Os bebedores frequentemente não entendem por que acordaram em um apartamento estranho, ou onde está seu celular. Chegou a hora de adicionar um detalhamento de log.

Vamos começar o log com o processo de servir:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Antes de servir com {_hand} e {_bottle}");
        //Lógica de negócios do servir ...
        _log.Log($"Depois de servir com {_hand} e {_bottle}");
    }
}

Encapsulando-a em PourOperation, agimos sabiamente em termos de responsabilidade e encapsulamento, mas agora temos um impasse com o princípio da variabilidade. Além da própria operação, que pode mudar, a própria logagem também se torna variada. Teremos que separar e criar um logador especial para a operação de derramamento:

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{
             //... lógica de negócios
             _log.LogAfter(_hand, _bottle);
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e);
        }
    }
}

Um leitor atento notará que LogAfter, LogBefore e OnError também podem mudar separadamente e, seguindo a lógica das ações anteriores, criará três classes: PourLoggerBefore, PourLoggerAfter e PourErrorLogger.

E lembrando que há três operações para o bebedor — obtemos nove classes de logagem. No final, todo o bebedor consiste em 14 (!!!) classes.

Hipérbole? Dificilmente! O homem-macaco com a granada de decomposição desmembrará o 'derramador' em jarra, copo, operadores de derramamento, serviço de fornecimento de água, modelo físico de colisão de moléculas, e no próximo trimestre tentará desvendar as dependências sem variáveis globais. E acredite — ele não vai parar.

É exatamente nesse ponto que muitos chegam à conclusão de que o SRP — é uma fábula dos reinos cor-de-rosa, e começam a ser enganados...

… sem nunca saber da existência da terceira definição de SRP:

«O Princípio da Responsabilidade Única afirma que coisas que são semelhantes para modificação devem ser mantidas em um só lugar«. ou “O que muda junto deve ser mantido em um só lugar”

Ou seja, se mudamos a logagem da operação, devemos mudá-la em um só lugar.

Esse é um ponto muito importante — já que todas as explicações sobre o SRP feitas anteriormente falavam que devemos dividir tipos, enquanto eles ainda podem ser divididos, ou seja, impunham uma 'restrição superior' ao tamanho do objeto, agora estamos falando também sobre uma 'restrição inferior'. Em outras palavras, O SRP não apenas exige 'dividir enquanto ainda é possível', mas também não exagerar — 'não desmembrar coisas interligadas'. Esta é uma grande batalha entre a navalha de Occam e o homem-macaco!

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

Agora deve ser mais fácil para o bebedor. Além de não precisar dividir o logador IPourLogger em três classes, também podemos unir todos os logadores em um único 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, a logação para ele já estará pronta. O código das operações em si é limpo e livre de ruídos de infraestrutura.

Como resultado, temos 5 classes para resolver a tarefa de consumo:

  • Operação de enchimento
  • Operação de consumo
  • Operação de acompanhamento
  • Logger
  • Facade de bebedor

Cada uma delas é responsável estritamente por uma funcionalidade, tendo uma única razão para mudança. Todas as regras semelhantes para alteração estão próximas.

Exemplo da vida real

Certa vez, estávamos escrevendo um serviço de registro automático de cliente b2b. E surgiu um método GOD com 200 linhas de conteúdo semelhante:

  • Vá ao 1C e crie uma conta
  • Com essa conta, vá ao módulo de pagamentos e crie-a lá
  • Verifique se a conta com essa numeração não foi criada no principal servidor
  • Crie uma nova conta
  • Adicione o resultado do registro no módulo de pagamentos e o número do 1C ao serviço de resultados do registro
  • Adicione à tabela a informação da conta
  • Crie um número de ponto para este cliente no serviço de pontos. Passe o número da conta 1C para este serviço.

E havia cerca de 10 operações de negócios com uma interconexão terrível nesta lista. O objeto da conta era necessário para quase todos. O identificador do ponto e o nome do cliente eram necessários em metade das chamadas.

Depois de uma hora de refatoração, conseguimos separar o código de infraestrutura e alguns detalhes do trabalho com a conta em métodos/classes separados. O método God ficou mais leve, mas ainda restavam 100 linhas de código que não queriam se desenrolar.

Apenas após alguns dias, veio a compreensão de que a essência deste método 'aliviado' é o próprio algoritmo de negócios. E que a descrição inicial do detalhamento foi bastante complexa. E que a tentativa de quebrar esse método em partes seria uma violação do SRP, e não o contrário.

Formalismo.

Chegou a hora de deixar nosso bebedor em paz. Seque as lágrimas — certamente voltaremos a ele algum dia. Mas agora formalizemos os conhecimentos deste artigo.

Formalismo 1. Definição do SRP

  1. Separe os elementos de modo que cada um deles tenha uma única responsabilidade.
  2. Responsabilidade se traduz como 'razão para mudança'. Ou seja, cada elemento tem apenas um motivo para mudança, em termos de lógica de negócios.
  3. Mudanças potenciais na lógica de negócios devem ser localizadas. Elementos alteráveis em sincronia devem estar próximos.

Formalismo 2. Critérios necessários de autoavaliação.

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

1) Pergunte a si mesmo — o que faz esta classe/método/módulo/serviço. 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) A correção de um certo bug ou a adição de uma nova funcionalidade deve afetar o mínimo de arquivos/classes. Idealmente — um.

explicações

Como a responsabilidade (por uma funcionalidade ou bug) está encapsulada em um único arquivo/classe, você sabe exatamente onde procurar e o que corrigir. Por exemplo: a funcionalidade de mudar a saída do registro de operações exigirá a mudança apenas do registrador. Não é necessário percorrer todo o restante do código.

Outro exemplo — adicionar um novo controle de UI, similar aos anteriores. Se isso faz você adicionar 10 entidades diferentes e 15 conversores diferentes — parece que você “overcomplicou”.

3) Se vários desenvolvedores estão trabalhando em funcionalidades diferentes do seu projeto, a probabilidade de um conflito de merge, ou seja, a probabilidade de que um mesmo arquivo/classe seja alterado por vários desenvolvedores ao mesmo tempo — é mínima.

explicações

Se ao adicionar uma nova operação "Derramar vodca embaixo da mesa" você precisa afetar o registrador, a operação de beber e derramar — parece que as responsabilidades estão mal divididas. Certamente, isso nem sempre é possível, mas devemos tentar reduzir esse índice.

4) Ao fazer uma pergunta esclarecedora sobre a lógica de negócios (de um desenvolvedor ou gerente), você vai diretamente a uma única classe/arquivo e obtém informações apenas dali.

explicações

Funcionalidades, regras ou algoritmos estão compactamente escritos, cada um em um único lugar, e não espalhados como flags por todo o código.

5) A nomeação é clara.

explicações

Nossa classe ou método é responsável por algo único, e essa responsabilidade é refletida em seu nome.

AllManagersManagerService — provavelmente, uma classe Deus.
LocalPayment — provavelmente, não.

Formalismo 3. Metodologia de desenvolvimento "Ockham-first".

No início do projeto, o homem-macaco não conhece nem sente todas as sutilezas da tarefa em questão e pode cometer um erro. Existem várias maneiras de errar:

  • Criar objetos excessivamente grandes, unindo diferentes responsabilidades
  • Fracionar, dividindo uma única responsabilidade em muitos tipos diferentes
  • Definir incorretamente os limites de responsabilidades

É importante lembrar da regra: "errar é melhor em excesso", ou "se não tiver certeza — não fracionar". Por exemplo, se a sua classe acumula duas responsabilidades — ela ainda é compreensível e pode ser dividida em duas com uma mínima alteração no código do cliente. Reunir um copo a partir de cacos de vidro geralmente é mais difícil devido ao contexto espalhado por vários arquivos e à falta de dependências necessárias no código do cliente.

Hora de concluir

O escopo de aplicação do SRP não se limita à OOP e SOLID. Ele se aplica a métodos, funções, classes, módulos, microsserviços e serviços. Ele é aplicável tanto em desenvolvimento "simples e direto" quanto em "ciência de foguetes", tornando o mundo um pouquinho melhor em todos os lugares. Ao refletir, isso é quase um princípio fundamental de toda a engenharia. Engenharia mecânica, sistemas de controle, e de fato, todos os sistemas complexos — são construídos a partir de componentes, e a ‘não fracionamento’ priva os engenheiros de flexibilidade, a ‘fracionamento’ — de eficiência, e limites incorretos — de lógica e paz de espírito.

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

O SRP não foi inventado pela natureza e não é parte de uma ciência exata. Ele emerge das nossas limitações biológicas e psicológicas. É apenas uma forma de controlar e desenvolver sistemas complexos usando a mente humana-macaco. Ele nos ensina como decompor um sistema. A formulação original exigia considerável habilidade de telepatia, mas espero que este artigo tenha ajudado a dissipar um pouco da névoa.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster