Princípio de responsabilidade única, ele é o princípio de responsabilidade única,
ele é o princípio da variabilidade uniforme - um sujeito extremamente escorregadio de entender e uma pergunta tão nervosa na entrevista de um programador.
O primeiro conhecimento sério desse princípio ocorreu para mim no início do primeiro ano, quando os jovens e os verdes foram levados para a floresta para formar verdadeiros alunos das larvas.
Na floresta, fomos divididos em grupos de 8 a 9 pessoas em cada um e organizamos uma competição - qual grupo beberá uma garrafa de vodka mais rapidamente, desde que a primeira pessoa do grupo derrame vodka em um copo, as segundas bebidas e a terceira morda. Após concluir sua operação, a unidade fica no final da fila do grupo.
O caso em que o tamanho da fila era múltiplo de três e era uma boa implementação do SRP.
Definição 1. Responsabilidade única.
A definição oficial do princípio da responsabilidade única (SRP) sugere que cada objeto tem sua própria responsabilidade e razão de existência, e essa responsabilidade possui apenas uma.
Considere o objeto Tippler.
Para cumprir o princípio do SRP, dividimos as responsabilidades em três:
- Um derrama ( PourOperation )
- Um drinques ( DrinkUpOperation )
- Um lanche ( TakeBiteOperation )
Cada um dos participantes do processo é responsável por um componente do processo, ou seja, tem uma responsabilidade atômica - beber, derramar ou dar uma mordida.
A bebida, por sua vez, é a fachada para essas operações:
lass Tippler { //... void Act(){ _pourOperation.Do() // _drinkUpOperation.Do() // _takeBiteOperation.Do() // } }
Porque
O programador humano escreve o código para o homem-macaco, e o homem-macaco é desatento, estúpido e sempre com pressa em algum lugar. Ele pode manter e entender cerca de 3 a 7 termos por vez.
No caso de bebida alcoólica, esses termos são três. No entanto, se escrevermos o código com uma folha, mãos, óculos, massacres e debates intermináveis sobre política aparecerão nele. E tudo isso estará no corpo de um método. Tenho certeza que você viu esse código em sua prática. Não é o teste mais humano para a psique.
Por outro lado, o homem-macaco é preso por modelar objetos do mundo real em sua cabeça. Em sua imaginação, ele pode juntá-los, coletar novos objetos e desmontá-los da mesma maneira. Imagine um modelo de carro antigo. Você pode abrir a porta com sua imaginação, desaparafusar a guarnição da porta e ver os mecanismos de elevação da janela, dentro dos quais haverá engrenagens. Mas você não pode ver todos os componentes da máquina ao mesmo tempo, em uma "lista". Pelo menos o "homem macaco" não pode.
Portanto, 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 sai pela porta e nos modernos - a falha na eletrônica da trava impede que o motor dê partida, o que ocorre durante o reparo.
Portanto, o SRP é um princípio que explica COMO decompor, ou seja, onde desenhar a linha de separação .
Ele diz que a decomposição deve basear-se no princípio da separação da "responsabilidade", isto é, de acordo com as tarefas de vários objetos.
Vamos voltar à bebida e às vantagens que uma pessoa macaco obtém ao se decompor:
- O código tornou-se extremamente claro em todos os níveis.
- Vários programadores podem escrever código de uma vez (cada um escreve um elemento separado)
- O teste automatizado é simplificado - quanto mais simples o elemento, mais fácil é testar
- Dessas três operações, no futuro, você pode adicionar um glutão (usando apenas TakeBitOperation ), um alcoólatra (usando apenas DrinkUpOperation diretamente da garrafa) e satisfazer muitos outros requisitos de negócios.
E, claro, os contras:
- Terá que criar mais tipos.
- Um bebedor bebe pela primeira vez algumas horas depois do que podia
Definição 2. Variabilidade unificada.
Permita cavalheiros! A classe de beber também cumpre uma única responsabilidade - 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 elevar os pingüins tombados no poste.
Considere duas implementações de bingo. A primeira, mencionada acima, contém três classes - despeje, beba e dê uma mordida.
O segundo é escrito através da metodologia Forward e Only Forward e contém toda a lógica do 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 as classes, do ponto de vista de um observador externo, parecem exatamente iguais e cumprem a única responsabilidade de "beber".
Vergonha!
Em seguida, navegamos na Internet e descobrimos outra definição de SRP - o princípio da variabilidade uniforme.
Esta definição afirma que " o módulo tem um e apenas um motivo para a mudança ". Ou seja, "Responsabilidade é uma ocasião para mudança".
Agora tudo se encaixa. Separadamente, você pode alterar os procedimentos de vazamento, bebida e mordida, e na própria bebida só podemos alterar a sequência e a composição das operações, por exemplo, mover o lanche antes de beber ou adicionar uma leitura de brinde.
Na abordagem Forward e Only Forward, tudo o que pode ser alterado é alterado apenas no método Act . Pode ser legível e eficaz no caso em que há pouca lógica e raramente muda, mas geralmente termina com métodos terríveis de 500 linhas cada, com mais números if do que o necessário para a entrada da Rússia na OTAN.
Definição 3. Localização de mudanças.
Os bebedores geralmente não entendem por que acordaram no apartamento de outra pessoa ou onde está o celular. É hora de adicionar log detalhado.
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}"); } }
Encapsulando-o no PourOperation , agimos com sabedoria em termos de responsabilidade e encapsulamento, mas agora, com o princípio da variabilidade, estamos envergonhados. Além da operação em si, que pode ser alterada, o próprio registro se torna variável. Teremos que separar e criar um logger 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) } } }
Um leitor meticuloso notará que LogAfter , LogBefore e OnError também podem ser alterados individualmente e, por analogia com as etapas anteriores, criará três classes: PourLoggerBefore , PourLoggerAfter e PourErrorLogger .
E lembrando que existem três operações para uma compulsão - temos nove classes de registro. Como resultado, toda a bebida consiste em 14 (!!!) classes.
Hipérbole? Dificilmente! Um homem-macaco com uma granada de decomposição esmagará o “vazador” em um decantador, 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á desvendar as dependências sem variáveis globais. E acredite em mim - ele não vai parar.
É neste ponto que muitos chegam à conclusão de que os SRPs são contos dos reinos cor-de-rosa e partem para torcer o macarrão ...
... nunca sabendo da existência da terceira definição de Srp:
" Coisas semelhantes às mudanças devem ser armazenadas em um só lugar ." ou " O que muda juntos deve ser mantido em um só lugar "
Ou seja, se alterarmos o log da operação, devemos alterá-lo em um só lugar.
Este é um ponto muito importante - uma vez que todas as explicações SRP acima mencionadas disseram que os tipos devem ser divididos enquanto são divididos, ou seja, impuseram uma "restrição superior" ao tamanho do objeto, e agora estamos falando de um "limite inferior" . Em outras palavras, o SRP não apenas exige "esmagamento ao esmagar", mas também não exagera - "não esmague coisas vinculadas" . Não complique desnecessariamente. Esta é a grande batalha da navalha de Occam com o homem-macaco!
Agora a bebida deve ser mais fácil. Além de não dividir o logger do IPourLogger em três classes, também podemos combinar todos os loggers 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 o quarto tipo de operação for adicionado a nós, o registro estará pronto para isso. E o código das operações em si é 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 atolamento
- Logger
- Fachada dos Boolers
Cada um deles é responsável estritamente por uma funcionalidade, tem um motivo para a mudança. Todas as regras semelhantes às alterações estão próximas.
Exemplos da vida real
Serialização e desserializaçãoComo parte do desenvolvimento do protocolo de transferência de dados, é necessário serializar e desserializar algum tipo de "Usuário" em uma string.
User{ String Name; Int Age; }
Você pode pensar que a serialização e a desserialização precisam ser feitas em classes separadas:
UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} }
Como cada um deles tem sua própria responsabilidade e uma razão para a mudança.
Mas eles têm um motivo comum de mudança - "alterando o formato da serialização de dados".
E ao alterar esse formato, a serialização e a desserialização sempre mudam.
De acordo com o princípio de localizar alterações, devemos combiná-las em uma classe:
UserSerializer{ String deserialize(User){...} User serialize(String){...} }
Isso evita a complexidade desnecessária e a necessidade de lembrar que toda vez que você altera o serializador, você precisa se lembrar do desserializador.
Contar e salvarVocê precisa calcular a receita anual da empresa e salvá-la no arquivo C: \ results.txt.
Resolvemos isso rapidamente com um método:
void SaveGain(Company company){ // // }
Já na definição da tarefa, está claro que existem duas subtarefas - "Calcular receita" e "Salvar receita". Cada um deles tem um motivo para alterações - "uma alteração na metodologia de cálculo" e "uma alteração no formato de salvamento". Essas alterações não se sobrepõem. Além disso, não podemos responder monossilábicamente à pergunta - “o que o método SaveGain faz?”. Este método AND calcula a receita E salva os resultados.
Portanto, você precisa dividir esse método em dois:
Gain CalcGain(Company company){..} void SaveGain(Gain gain){..}
Prós:
- pode ser testado separadamente CalcGain
- mais fácil de localizar bugs e fazer alterações
- legibilidade do código aumentada
- o risco de erro em cada um dos métodos é reduzido devido à sua simplificação
Lógica de negócios sofisticadaCerta vez, escrevemos um serviço para registro automático de um cliente B2B. E havia um método GOD com 200 linhas de conteúdo semelhante:
- Vá para 1C e obtenha uma conta
- Com esta conta, vá para o módulo de pagamento e chegue lá
- Verifique se uma conta com essa conta não foi criada no servidor principal
- Crie uma nova conta
- O resultado do registro no módulo de pagamento e o número 1c são adicionados ao serviço de resultados do registro
- Adicionar informações da conta a esta tabela
- Crie um número de ponto para este cliente no serviço de pontos. Dê a esta conta de serviço número 1s.
Havia cerca de 10 operações comerciais com uma conexão terrível nesta lista. O objeto da conta era necessário para quase todos. A identificação 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 de infraestrutura e algumas nuances do trabalho com a conta em métodos / classes separados. O método God ficou mais fácil, mas restavam 100 linhas de código que não queriam ser desvendadas.
Apenas alguns dias depois chegou a compreensão de que a essência desse método "aliviado" é o algoritmo de negócios. E que a descrição inicial de TK era bastante complicada. E é uma tentativa de quebrar esse método em pedaços que serão uma violação do SRP, e não vice-versa.
É hora de deixar nossa bebida em paz. Limpe as lágrimas - nós definitivamente voltaremos a ela de alguma forma. Agora formalizamos o conhecimento deste artigo.
- Separe os elementos para que cada um deles seja responsável por uma coisa.
- Responsabilidade significa "causa de mudança". Ou seja, cada elemento tem apenas um motivo para a mudança, em termos de lógica de negócios.
- Potenciais alterações na lógica de negócios. deve ser localizado. Itens mutáveis juntos devem estar próximos.
Não atendi a critérios suficientes para a implementação do SRP. Mas existem condições necessárias:
1) Faça uma pergunta a você mesmo - o que essa classe / método / módulo / serviço faz? você deve respondê-lo com uma definição simples. (graças a Brightori )
explicaçõesNo entanto, às vezes é muito difícil encontrar uma definição simples
2) Corrigir um erro ou adicionar um novo recurso afeta o número mínimo de arquivos / classes. Idealmente, um.
explicaçõesComo a responsabilidade (por um recurso ou bug) é encapsulada em um único arquivo / classe, você sabe exatamente onde procurar e o que editar. Por exemplo: o recurso de alterar a saída do log de operação exigirá a alteração apenas do logger. Não é necessário executar o restante do código.
Outro exemplo é a adição de um novo controle de interface do usuário semelhante aos anteriores. Se isso força você a adicionar 10 entidades diferentes e 15 conversores diferentes - parece que você “quebrou”.
3) Se vários desenvolvedores estão trabalhando em diferentes recursos do seu projeto, a probabilidade de um conflito de mesclagem, ou seja, a probabilidade de vários desenvolvedores mudarem o mesmo arquivo / classe ao mesmo tempo, é mínima.
explicaçõesSe, ao adicionar uma nova operação "Despeje a vodka embaixo da mesa", você precisará tocar o lenhador, a operação de beber e derramar - então parece que as responsabilidades estão divididas de forma torta. Obviamente, isso nem sempre é possível, mas você precisa tentar reduzir esse número.
4) Ao esclarecer uma pergunta sobre lógica de negócios (de um desenvolvedor ou gerente), você entra estritamente em uma classe / arquivo e recebe informações apenas a partir daí.
explicaçõesRecursos, regras ou algoritmos são escritos de forma compacta, cada um em um único local, e não espalhados por sinalizadores pelo espaço de código.
5) A nomeação é clara.
explicaçõesNossa classe ou método é responsável por uma coisa, e a responsabilidade se reflete em seu nome.
AllManagersManagerService - provavelmente, classe de Deus
LocalPayment - provavelmente não
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 causar um erro. Você pode cometer erros de maneiras diferentes:
- Faça objetos muito grandes colando responsabilidades diferentes
- Divida, dividindo uma única responsabilidade em muitos tipos diferentes
- Limites de responsabilidade definidos incorretamente
É importante lembrar a regra: "é melhor cometer um grande erro" ou "não tenho certeza - não se divida". Se, por exemplo, sua classe coletar duas responsabilidades, ainda será compreensível e poderá ser dividida em duas com uma alteração mínima no código do cliente. Coletar um copo de fragmentos 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 terminar
O escopo do SRP não se limita ao OOP e ao SOLID. É aplicável a métodos, funções, classes, módulos, microsserviços e serviços. Isso se aplica ao desenvolvimento "figax-figax-and-in-prod" e "rocket-sainz", tornando o mundo um pouco melhor em todos os lugares. Se você pensar bem, esse é quase o princípio fundamental de toda a engenharia. A engenharia mecânica, os sistemas de controle e, de fato, todos os sistemas complexos são construídos a partir de componentes, e a "fragmentação incompleta" priva os projetistas de flexibilidade, "fragmentação" - de eficiência e limites incorretos - da razão e tranqüilidade.

O SRP não é inventado pela natureza e não faz parte da ciência exata. Ele sai das nossas limitações biológicas e psicológicas, e é apenas uma maneira de controlar e desenvolver sistemas complexos usando o cérebro de um macaco humano. Ele nos diz como decompor o sistema. A redação original exigia bastante telepatia, mas espero que este artigo tenha dissipado levemente a cortina de fumaça.