Single responsibility, o princípio deturpado

Out 27 (pt)

Softwares são feitos por humanos, para humanos e máquinas. E humanos erram, consequentemente fazendo a máquina falhar também.

Dado esse contexto, ao longo do tempo foram desenvolvidos e estabelecidos diversos princípios e padrões de desenvolvimento de software. E todos eles tem uma coisa em comum: simplificar para que consigamos trabalhar utilizando menos esforço cognitivo, dado que a nossa mente é limitada e os problemas que temos que resolver muitas vezes já são complexos o suficiente apenas em seu conceito, sem nenhuma linha de código escrita.

Isso é um insight muito importante que tive depois de passar por diversos projetos ao longo da minha carreira, no começo não é fácil entender apesar de parecer simples:

  • SOLID
  • MVC
  • DDD (Domain driven design)
  • Clean Architecture
  • Clean Code
  • Refactoring / Code Smells
  • TDD
  • Redux
  • Microservices
  • Microfrontends
  • Orientação a objetos (Encapsulamento, herança, polimorfismo, modelos anêmicos, sobrecarga …)
  • Programação funcional (Monads, funtores, closures, currying, teoria das categorias, funções puras …)
  • Actor Model
  • Lei de Demeter
  • Serveless
  • Web components

E além do software:

  • Agile
  • Scrum
  • Kanbam

Pra que a gente aprende e estuda essa p*** toda?

A resposta pode parecer óbvia:

  • "Aprendemos e estudamos para utilizar no nosso dia a dia, sempre, e essas são as melhores práticas."

Mas será que essa é a resposta certa mesmo?

Não.

Existem contextos em que não precisamos utilizar nada disso e vários outros que nos beneficiaríamos utilizando apenas alguns desses padrões e práticas, não todos. Dessa forma, a minha resposta hoje em dia, seria:

  • "Aprendemos e estudamos para saber onde e quando utilizar no nosso dia a dia"

Acontece que essa resposta só surge junto com a vivência, a realidade. E muitas vezes no início de carreira e durante muito tempo nos pegamos utilizando padrões apenas por ter estudado recentemente e assumir que é aquilo que devemos fazer, afinal é um padrão! É o correto!

Essa última frase resulta em dois dos maiores problemas que particulamente enfrentei ao longo da carreira, vindo de código legado (ou a partir de mim mesmo):

  • Overengineering: Engenharia e padrões demais para um problema simples
  • Underengineering (acabei de inventar esse termo): Engenharia e padrões de menos para um problema complexo

E esses problemas, são coisas que cursos de "javascript full-stack", frameworks ou linguagens geralmente não vão nos ensinar.

Todo padrão foi criado a partir de um problema. Se não passamos por esse problema, não precisamos do padrão. E reconhecer fortemente o problema é tão ou mais importante do que conhecer os detalhes de um padrão. Nosso trabalho como Software Engineer é justamente trabalhar sem "over" e sem "under" apenas com o "engineer". Primeiro reconhecer os problemas, depois encaixar os padrões (e não a ordem inversa).

Dado essa introdução, gostaria de falar agora sobre um "caso ímpar" e muito curioso:

  • Um padrão / princípio onde é muito comum a galera não reconhecer o problema que ele soluciona de fato e nem mesmo como ele funciona, mas sai "utilizando" (e "pregando") em todo lugar. Provavelmente isso é resultado de um "telefone sem fio" gigante entre desenvolvedores (um fala para o outro, o outro escuta e fala para mais um seguidamente e nenhum deles busca de fato entender a fundo), outro ponto que pode ter resultado nisso é a simplicidade do seu nome:

Single Responsibility Principle (O princípio da responsabilidade única)

Diferente de "Princípio da substituição de Liskov", "Chain of responsibility" ou "Lei de Demeter", é muito fácil lermos "Princípio da responsabilidade única" e pensar:

"Opa! Princípio da responsabilidade única, essa classe faz mais do que uma coisa! Ela tem mais de uma responsabilidade! Vamos transforma-la em duas"

É simples. Métodos, classes e todo organismo dentro de um software deve ter "uma única responsabilidade", não é mesmo?

Depende do que entendemos como responsabilidade

Para exemplificar, gosto muito de utilizar o dicionário:

  • Responsabilidade: Obrigação de responder pelas ações próprias ou dos outros. Caráter ou estado do que é responsável. Dever de responder pelo próprio comportamento.

Marquei a última sentença pois será mais simples de utilizar com o exemplo abaixo:

Imagine que você é um entregador ou uma entregadora de pizza, qual é a sua responsabilidade e qual é o comportamento pelo qual você responde?

Imaginou?

Eu diria que:

  • Responsabilidade: Entregar a pizza
  • Comportamento pelo qual responde: Guardar a pizza na mochila de forma cuidadosa para que ela chegue inteira, ligar a moto, acelerar a moto, pilotar com segurança se atentando as leis de trânsito, interfonar quando chegar e ser educado/a com o cliente.

Temos mais de uma responsabilidade? Ou temos comportamentos pelos quais respondemos dado a nossa responsabilidade?

Usando exemplos do mundo real fica mais claro, correto?

Então vou piorar o exemplo, nosso próprio corpo de entregador/a de pizza tem orgãos, e eles também tem suas responsabilidades:

  • O coração bate e responde pela circulação do sangue
  • O pulmão respira e responde por absorver O2 e eliminar CO2 do ar respirado
  • Os rins filtram e respondem pela quantidade de detritos no nosso sangue

Sabendo disso, como entregadores/as de pizza temos muito mais responsabilidades do que imaginamos? Também temos que ter nosso corpo funcionando bem para conseguir exercer a profissão correto?

Como entreagores/as de pizza, respondemos pelo comportamento e responsabilidades do nosso corpo?

Nessa altura você deve estar bastante confuso ou confusa. As coisas começaram a tomar proporções grandes que provavelmente fizeram você ter um esforço cognitivo muito maior para entender toda a complexidade que envolve o "princípio da responsabilidade única", o significado de responsabilidade e comportamento.

Olhando o primeiro exemplo parece claro o que é responsabilidade e comportamento, mas quando te trouxe o segundo provavelmente você percebeu que isso pode ser infinito, e tomar proporções gigantes.

Poderia finalizar o artigo aqui, para bons entendedores e endendedoras meia palavra basta

Mas vou continuar. Fazendo uma "analogia da analogia" a nível de software, essa confusão fica exposta da pior maneira possível e aumenta da mesma forma o nosso esforço cognitivo, indo contra simplificar para que consigamos trabalhar utilizando menos esforço cognitivo.

Se isso fosse um software, eu não me espantaria de abrir os arquivos e ver coisas como:

  • "EntregadorDePizza"
  • "EntregadorDePizzaBuilder"
  • "LigadorDeMoto"
  • "MantemRimFuncionandoService"
  • "AceleradorDeMoto"
  • "PilotagemComSegurança"
  • "CuidadosComAPizza.Cuidar()"
  • "BatimentosDoCoração.Bater()"
  • "BatimentosDoCoraçãoFactory.create()"
  • "EntregarPizzaFacade"
  • "PizzaServices"

Aquele "código gostoso" com alta granularidade, várias injeções de dependências e várias classes de apenas um método com "cinco, dez linhas de código", "respeitando a Single Responsibility Principle até o final"!

Muito fácil de dar manutenção, inclusive (contêm ironia)

No final das contas, esse tipo de situação causa um software que é "estruturado lateralmente" (não existe mais Orientação a Objetos ou Programação funcional). Ao invés de termos uma leitura de código de cima para baixo, vamos acabar abrindo "20" arquivos na nossa IDE para entender uma única regra de negócio que muitas vezes pode representar comportamentos de uma única responsabilidade (em "20" arquivos).

Ao invés de encontrar o que procuramos em um ponto central, em um arquivo único, temos que procurar em vários arquivos e depois ligá-los mentalmente.

É a mesma coisa que juntar peças de um quebra cabeça gigante.

E ainda falo sobre o menos pior dos casos! Porque essa granularidade pode acabar partindo para projetos diferentes, microserviços ou microfrontends. Imagina ter que abrir 10 microserviços + 5 microfrontends?

O que acontece é que muita gente ainda não faz idéia da diferença entre responsabilidade e comportamento.

O próprio Uncle Bob (criador do princípio) disse em um de seus livros:

"De todos os princípios SOLID, o princípio da responsabilidade única provavelmente é o menos compreendido. Isso se deve, possivelmente ao seu nome bastante inadequado. Em geral, ao escutarem esse nome, os programadores imaginam logo que todos os módulos devem fazer apenas uma coisa"

Robert C. Martin - Clear Architecture (Arquitetura Limpa) p/ 62

Voltando ao nosso exemplo, temos apenas uma responsabilidade: Entregar a pizza.

Porque podemos afirmar isso?

O método e o comportamento são independentes da responsabilidade. Continuamos sendo apenas entregadores/as de pizzas com a responsabilidade única de entregar pizzas independente de como fazemos isso.

Posso por exemplo começar a entregar de carro ao invés de moto. E se alguém precisar aprender a entregar pizzas, esse alguém vai me procurar e eu vou ensinar claramente como.

"Um módulo é apenas um conjunto coeso de funções e estruturas de dados. Esta palavra "coeso" sugere o SRP. Coesão é a força que amarra o código responsável a um único ator"

Robert C. Martin - Clean Architecture (Arquitetura Limpa) p/63

É completamente coeso, por exemplo, termos uma classe EntregadorDePizza com um método entregar_pizza que recebe três objetos: pizza, cliente e moto. E este método ter todo aquele comportamento que listamos. Afinal, não vamos procurar saber como o entregador entrega a pizza em outro ponto do código:

Apenas um esboço …

class EntregadorDePizza
  def entregar_pizza(pizza, cliente, moto)
    guardar_na_mochila(pizza)
    moto.ligar(chave_da_moto)
    moto.pilotar_com_segurança_até(cliente.endereço)
    interfonar()
    ser_educado_com_o(cliente)
  end

  private
	
  def chave_da_moto
	
  # ...

  def guardar_na_mochila(pizza)
	
  # ...
end

Single Responsibility Principle é sobre isso.

É sobre responsabilidade, comportamento e como os descrevemos de forma coesa. Não sobre "fazer uma coisa só".

… "Esse princípio é importante no momento em que há uma alteração em alguma funcionalidade do software. Quando isso ocorre, o programador precisa procurar pelas classes que possuem a responsabilidade a ser modificada. Supondo uma classe que possua mais de uma razão para mudar, isso significa que ela é acessada por duas partes do software que fazem coisas diferentes. Fazer uma alteração em uma das responsabilidades dessa classe pode, de maneira não intencional, quebrar a outra parte de maneira inesperada. Isso torna o projeto de classes frágil"

Maurício Aniche - Test-Driven Development p/ 204

Quebrando nosso Single Responsibility Principle e os problemas que o originaram

Imagine que você é software engineer da pizzaria e precisa mudar no sistema a forma como a pizza é entregue (comportamento), em quais classes você procuraria primeiro?:

  1. EntregarPizza ou EntregadorDePizza
  2. Cozinha ou FornoDaCozinha
  3. CalcularContabilidade ou FolhaDePagamento

Com certeza você escolheu a opção 1, porque é o mais coeso.

O primeiro problema que o SRP corrige é justamente a baixa coesão. Se tivessemos uma classe chamada CalcularContabilidade que tivesse o comportamento de "esquentar o forno da cozinha" estaríamos violando o SRP.

Nenhum programador ou programadora vai pensar em abrir uma classe CalcularContabilidade para mudar a "temperatura do forno da cozinha". É contra intuitivo. CalcularContabilidade teria 2 responsabilidades: cuidar da cozinha e da contabilidade. Teria razões diferentes para mudar.

O segundo maior problema é a complexidade que uma classe com muitas responsabilidades (lembrando que responsabilidade não é comportamento) pode ter. A tendência é a classe ficar difícil de dar manutenção e testar. Além de que dois contextos completamente diferentes podem interferir um no outro.

Obs:. Repare também que no exemplo da seção anterior, o método entregar_pizza da classe EntregadorDePizza sabe ligar a moto, mas não sabe como a moto funciona por dentro quando ele liga. O comportamento do maquinário da moto não é responsabilidade do EntregadorDePizza. Se o comportamento do maquinário tivesse nessa classe também, teríamos algo semelhante a essa imagem:

Sempre que quiséssemos dar manutenção na moto, teríamos que mexer no EntregadorDePizza. O que para pessoas sãs não faz o mínimo sentido.

A formula mágica para respeitar o SRP

  • Evite utilizar nomes genéricos dentro do seu software, como Services ou Manager.
  • Em caso de dúvidas, pergunte-se:
    • A classe / módulo Y que tem a responsabilidade X deve responder pelo comportamento Z?
  • Antes de sair codando, reserve um tempo para pensar em como traduzir sua regra de negócio para estruturas (classes, módulos e funções) com nomenclaturas coesas. Use palavras que são comuns no dia a dia da sua equipe. Gosto de pensar que um software bem escrito é aquele que uma pessoa não técnica (PO, PM e Stakeholders) consegue abrir os arquivos e entender superficialmente o que está acontecendo.

Resumindo

Essas são as 5 principais mensagens que enfatizo com esse artigo:

  • Responsabilidade não é o comportamento em si. É o dever de responder por um conjunto de comportamentos.
  • SRP é sobre responsabilidade, comportamento e como os descrevemos de forma coesa. Não sobre fazer uma coisa só.
  • Coesão, acima de tudo, tem a ver com o nome que damos as variáveis, classes, módulos, funções e métodos em relação a o que eles fazem e/ou são.
  • Todo e qualquer padrão ou prática serve para simplificar para que consigamos trabalhar utilizando menos esforço cognitivo, caso isso não aconteça, não devemos utilizar o padrão ou prática.
  • Cabe ao engenheiro de software, dado um determinado contexto, encontrar a solução adequada e ideal para o problema, identificando quais padrões utilizar ou não, dado o prazo, tamanho do projeto e os objetivos de negócio.

You dont understand the single responsibility principle (Hackernoon)

Single responsibility principle: how can i avoid code fragmentation (Stack Exchange Question)

The Single Responsibility Principle (David Tanzer)

Single reponsibility principle (Blog Clean Coder / Uncle Bob)

Livro Clean Architecture (Link de compra da Amazon)

Cohesion (computer science) (Wikipedia)

Livro TDD com Ruby (Casa do código)

Livro Clean Code (Link da Amazon)



Ei, o que achou desse artigo?

Compartilhe e dê sua opinião clicando em uma das redes abaixo:


Muito obrigado!