Como o tal do SOLID pode melhorar seu projeto orientado a objetos

Robert C. Martin, um dos heróis Kiconianos

É chocante ver o quão subaproveitada a orientação a objetos é. Interessante é que só se percebe isto quando mudamos nossas leituras. No “Panteão Kiconiano de Heróis” há um chamado Robert C. Martin, que agrupou cinco princípios básicos do design orientado a objetos sob o acrônimo SOLID.

Quando tomei conhecimento deste acrônimo minha vida melhorou bastante e, acredito, a sua também a partir de agora caso este seja seu primeiro contato com o tema.

Mas antes de entrar no assunto, acredito que seja interessante fazer uma revisão do meu próprio desenrolar enquanto criador de objetos. Talvez nossa história seja similar e, nesta identificação eu lhe ajude de alguma maneira.

Meus primeiros objetos

Quando ouvi falar em orientação a objetos a primeira coisa que me veio à cabeça foi reutilização de código, mas não esta reutilização bonita que as pessoas estufam o peito pra falar. Meu raciocínio básico naquela época era:

“Hmm… bacana este negócio de orientação a objetos hein? Por que com isto eu posso pegar todo o meu código que fico repetindo em um monte de lugares, incluir em uma classe e, em seguida, simplesmente ir criando classes filhas desta. Assim, sempre que surgir alguma coisa que eu use em mais de um lugar, eu incluo apenas naquela classe e todas as outras terão acesso. Genial!”

Quando me lembro estufando o peito para falar isto naquela época não consigo conter as gargalhadas que dou de mim mesmo. Isto porque na minha cabeça sistemas deviam ser projetados tal como no diagrama abaixo:

Ah... o "reaproveitamento de código"

Pra que uma classe Object básica se eu podia ter a minha própria classe “Global”? E o mais bacana é que eu podia instanciar a classe Global e usá-la em diversos pontos diferentes do sistema. Um design “lindo”!

Foi quando ouvi falar de um princípio de design chamado de responsabilidade única (Single Responsability em inglês).

Responsabilidade Única (Single Responsability (SRO))

Este princípio diz o seguinte: uma classe deve possuir uma, e apenas uma responsabilidade, ou seja, deve ser especializada de tal modo que execute apenas uma tarefa e, de preferência, de uma forma bem feita. Eu olhava pro meu design original e uma vergonha imensa me consumia porque ficava nítido que a minha classe “Global” era quase divina, pois fazia um pouco de tudo.

Este termo foi cunhado por Robert C. Martin (aka Uncle Bob) em um texto chamado “The Single Responsability Principle“. Por responsabilidade, entenda uma “razão para que o código seja modificado”. Imagine por exemplo um relatório. Em um design original, poderíamos implementar uma classe que fosse responsável pela extração dos dados, processamento dos mesmos e, pra finalizar, a exposição dos dados. Resumindo, poderíamos implementar este objeto como uma “classona“. Nesta classe há três responsabilidades:

  • Extração dos dados
  • Processamento dos dados
  • Impressão dos resultados

Seguindo este princípio, poderíamos refatorar este monstrinho para que fosse composto por três classes, tal como no diagrama abaixo:

Como dizem aqui em Minas Gerais: "agora o trem tá chique!"

O ganho é imediato neste novo design, porque as consequências das mudanças está bem mais controlada. Se eu alterar a classe que extrai dados, apenas esta será afetada. O processador de dados pode até receber dados equivocados, mas como seu papel é processá-los, posso incluir um tratamento de erros que só diz respeito a si mesma. E também posso trocar a saída do meu relatório precisando fazer modificações em apenas uma classe.

E sabe o que é mais legal? Como o código fonte de cada classe é menor do que o daquela classona inicial, eu compreendo muito melhor o funcionamento de cada componente. E com isto os meus bugs diminuem bastante.

Fechado para modificações, aberto para expansão (Open Closed Principle (OCP))

Depois que aprendi o conceito de responsabilidade única minha vida melhorou bastante enquanto eu era um desenvolvedor solitário. Mas a partir do momento em que passei a trabalhar em equipes maiores, minha vida começou a se complicar pois nem sempre o meu código executava exatamente como eu queria. Era engraçado, meu processador de dados, por exemplo, muitas vezes me gerava resultados completamente malucos quando este deveria ser sempre o único.

Quando eu ia descobrir o resultado, descobria sempre que algum “engraçadinho” havia feito com meu sistema algo como o exposto no diagrama abaixo:

Equipe danadinha!

A culpa não era da equipe, mas minha. Projetei o sistema inicialmente de tal maneira que qualquer um poderia sobrescrever o método processe da classe Processador de Dados livremente. Como resultado, eu não tinha mais a uniformidade do conceito de processamento de dados neste sistema. Dependendo do contexto, isto pode ser ótimo, mas neste que estou aplicando é péssimo. O resultado em Java para este problema seria muito fácil de ser resolvido. Na primeira versão que liberei eu devia ter implementado o método processe na classe original como final. Voilá, ninguém mais o substituiria.

Mas por que na prática eu fecho para modificações uma classe? A razão é simples: porque assim eu posso desenvolver meu software como se fosse em camadas. Estando uma camada muito bem escrita e bem definida, eu tenho certeza de que todas as classes derivadas também funcionarão bem. As classes derivadas na prática poderíam apenas usar os métodos fechados e acrescentar novos comportamentos ao sistema conforme novas necessidades fossem surgindo.

Tio Bob tem um excelente artigo sobre isto neste link.

O princípio de substituição de Liskov (Liskov Substitution Principle)

O meu Processador de Dados semrpe envia os dados para o impressor de resultados e espera um valor inteiro como resposta que representa o sucesso ou falha da impressão. Normalmente este valor é positivo caso tudo ocorra bem e negativo caso contrário. Sendo assim, internamente na minha classe Processador de Dados há um método chamado imprima, chamado pelo método processe tal como no exemplo abaixo:


public final void processe() { // ah... aprendi com o princípio OCP

// bla bla bla

if (getImpressor().imprima(resultado) > 0) {
// execute uma saudação feliz
} else {
// avise ao presidente que estamos sofrendo um ataque nuclear
}

}

No caso, estamos lidando com uma convenção. Nosso processador de dados espera que nosso objeto impressor possua um comportamento bem definido. Se algum mal informado de nossa equipe resolver implementar uma classe de impressão que haja de maneira distinta, veremos um presidente tendo um ataque do coração.

Basicamente isto é o que chamamos de princípio de substituição de Liskov (só por curiosidade, Liskov é uma mulher, e se chama Barbara). No projeto de nossos sistemas, devemos proceder de tal maneira que, ao implementarmos nossas classes filhas, o façamos de tal maneira que as classes clientes não sofram surpresas desagradáveis como a que descrevi acima.

E adivinha? Uncle Bob explica este conceito muito melhor do que eu neste artigo.

Princípio de Segregação de Interfaces (Interface Segregation Principle (ISP))

Meu extrator de dados era muito bom. Tão bom que eu comecei a inserir em seu corpo uma série de outros métodos que eram usados por outras classes além do meu processador de dados. Terminei meu projeto com uma carinha similar à da imagem abaixo:

O extrator de dados mais popular do quarteirão

Conforme o extrator de dados foi crescendo, ficou nítido que sua interface também cresceu. E de repente meu Processador de Dados tinha de lidar com uma interface bem mais complexa do que a original. De repente esta classe tem conhecimento de outras que não devia sonhar com a existência, como por exemplo DadosMaravilhosos e DadosLindos. Lembra que um dos princípios básicos por trás da OO é o encapsulamento? Nosso processador só precisa conhecer o método extrairDados e a classe Dados.

O princípio de segregação de interface diz o seguinte: se uma interface começa a engordar, devemos parti-la em diversas novas interfaces de tal modo que cada cliente só conheça aquilo que de fato lhe diz respeito. Então modifiquei meu sistema para que ficasse como no diagrama abaixo:

Ficou chique!

Nesta segunda versão quebramos a interface original do Extrator de Dados em três: uma para cada cliente. Com isto tivemos uma série de ganhos interessantes:

  • Cada cliente só conhece as classes e os métodos de que realmente precisa
  • Temos um desenvolvimento realmente voltado para interfaces
  • Temos um menor acoplamento no sistema (ei, mais sobre isto no próximo princípio)

É interessante observar como que, na prática, adicionando um número maior de elementos no nosso sistema, acabamos por diminuir o custo de manutenção. Isto porque podemos trabalhar nas classes cliente isoladamente de uma forma mais segura, visto que temos um acoplamento menor.

Ei, Robert Martin tem um texto sobre o assunto também!

Princípio de Inversão de Dependências (Dependency Inversion Principle (DIP))

Observe o diagrama abaixo que corresponde à primeira versão evoluída do nosso sistema. Além dos problemas apresentados nos últimos quatro princípios, consegue identificar mais alguma falha?

Cadê o problema?

Suponha que um dia comercializemos nosso Processador de Dados, e que queiramos variar as formas de obtenção de dados ou de impressão de resultados. Como faríamos? Claro, poderíamos implementar alguma lógica dentro das classes relativas de tal modo que algum flag definisse qual a origem das informações e como gostaríamos de apresentar os resultados, certo?

Ou poderíamos também definir em tempo de execução qual classe instanciar, como fazemos por exemplo usando um container de injeção de dependências. São duas soluções, mas o problema persiste: nosso processador de dados conhece demais as suas dependências. O princípio de inversão de dependências diz o seguinte:

Um módulo de nível superior não deve depender diretamente de módulos de níveis inferiores, mas de abstrações (e módulos de níveis inferiores também devem depender de abstrações).
Abstrações não devem depender de detalhes de implementação, mas sim o contrário.

Este princípio é poderosíssimo, e é a base por trás do conceito de injeção de dependências. Primeiramente, o que é uma abstração? É a generalização de um comportamento excluindo todos os seus detalhes de implementação. No mundo da orientação a objetos, corresponde às interfaces ou classes abstratas/pai.

E o que é um módulo de nível superior? É aquele aonde normalmente se encontra encapsulada a lógica de negócio do nosso sistema. E de nível mais baixo? Aquele que não está diretamente ligado ao objetivo do sistema, como por exemplo nosso extrator de dados e o impressor de resultados.

Neste caso, você me pergunta: como eu arrumo este sistema? Simples, como no diagrama abaixo:

Repare: o Processador de Dados não precisa mais saber detalhes sobre a implementação do Impressor ou do Extrator. Ele só precisa lidar com as abstrações dos mesmos. Podemos agora substituir facilmente suas implementações sem o risco de obtermos comportamentos inesperados no nosso sistema. Resumindo: resolvemos o problema do alto acoplamento.

Um dos textos mais importantes da minha carreira é este: “The Dependency Inversion Principle” de você já sabe quem. Caso este meu post te leve a ler este artigo, me dou por realizado, porque foi um dos raros que literalmente mudou minha carreira. A propósito, eu tenho um artigo inteiro sobre isto neste link (e estou escrevendo um livro sobre o assunto).

E o que é o tal do SOLID hein?

São os cinco princípios que expus de uma maneira bem light acima:

Single Responsability Principle (Responsabilidade Única)
Open Closed Principle (Aberto para expansão, fechado para modificação)
Liskov Substitution Principle (Princípio de Substituição de Liskov)
Interface Segregation Principle (Princípio de Segregação de Interface)
Dependency Inversion Principle (Princípio de Inversão de Dependências)

Sugestão: leia cada um dos artigos que citei no corpo do texto e, semana a semana, dedique-se a pensar em apenas um destes. Eu te garanto: você se tornará um desenvolvedor muito, mas muito melhor.

Fica a dica ;)

28 comentários em “Como o tal do SOLID pode melhorar seu projeto orientado a objetos”

  1. Paulo Vitor

    Excelente post. Esse é um assunto que a princípio parece básico mas, é justamente nesses conceitos que a maioria das equipes de desenvolvimento esbarram e podem com toda certeza determinar o sucesso ou a falha de um projeto.

    1. Kico (Henrique Lobo Weissmann)

      Oi Paulo, que legal que gostou, valeu!

      Te dizer uma coisa: eu já vi projeto afundar justamente por desconhecimento destes princípios básicos.

  2. Marcos Vinícius

    Muito bom o artigo.
    Só acho que ficou devendo a parte do aberto para extensão no OCP. Ou não captei alguma coisa?

    Grande abraço!

    1. Kico (Henrique Lobo Weissmann)

      Oi Marcos,

      legal que tenha gostado, valeu.
      Sabe de uma coisa? Relendo o post, vejo que você tem razão.
      Sabe o que isto quer dizer né? :)

      Que vai haver um post neste blog em muito breve só sobre este assunto. Valeu pelo toque!

  3. No meu entendimento o uso desses “principios” nao garantem que um projeto vai ser melhor ao mesmo tempo que tambem nao garante que vai ser pior. Orientacao a objetos e um paradigma falho e desnecessario para um conjunto de aplicacoes (procure por programacao funcional). Um objeto pode ter uma responsabilidade mas conter varios comportamentos simultaneamente seja atraves da heranca multipla (procure por scala trait e mixin) ou atraves de “crosscutting concerns” (procure por aop). Composicao sempre e preferivel a heranca ainda assim existe o problema do diamante (procure por diamond problem) e mesmo assim nao existem garantias de um correto funcionamento geral (procure por Anti-composition Axiom). No mundo concorrente de hoje, de computacao concorrente, paralela, ubiqua e pervasiva, baseado em trocas de mensagens intensivamente, OO e SOLID sao facilmente substituiveis como “conceitos poderosos” por outras formas melhores de se resolver os problemas do diarios. (veja programacao declarativa). Ate mais,

    1. Kico (Henrique Lobo Weissmann)

      Opa, grande Leo!

      Sim, é fato, mas se for pra programar em OO, é melhor aplicando técnicas já consagradas (e comprovadas) do que não.

      No final das contas, é tudo uma questão de disciplina. Estes conceitos, que não devem ser tidos como mantras, servem mais pra nos guiar e apontar possíveis falhas do que como regras rígidas que devemos seguir em frente.

      Com relação à superioridade do funcional com relação à OO, bem: cada situação requer sua ferramenta. Se for o caso de se usar OO, que o seja conhecendo algumas práticas bacanas que sim, no final das contas, neste ponto, discordo de você, ajudam a ter um projeto melhor, visto que pelo menos servem como guia.

    2. Pobre é aquele que usa um canivete para enfrentar um urso, e uma katana para cortar uma sardinha.

  4. Lucas Sampaio

    O princípio de responsabilidade única é mais difícil de ser aplicado do que parece. Ainda sou bem novato em programação OO, quando comecei na faculdade pensar na melhor modelagem para as atividades era relativamente fácil, mas volta e meia me pegava fazendo classes maiores do que deveriam ser – e com mais de uma responsabilidade – ou criando código procedural dentro de classes.

    1. Kico (Henrique Lobo Weissmann)

      Oi Lucas, te digo mais.

      Todos os princípios são. É questão de treinar o tempo inteiro pra que eles passem a se tornar parte da nossa maneira de pensar.

      1. Lucas Sampaio

        Acho bastante interessante testar todas estas práticas de programação em diferentes linguagens, nem sempre uma boa prática em Java será uma boa prática em Python, e vice-versa.

  5. Ótimo artigo. Boa parte disso é repetição do que já vem sendo falado há anos, por diversos autores, com diversos termos diferentes, mas não se pode negar que são assuntos que valem a pena ser repetidos.

    Boa parte desses conceitos também mudaram minha vida. :)

    No caso da inversão de dependência, eu sou um tanto radical. Para mim abstração falada ali, no mundo da OO, resume-se apenas a interface. Ali é o único lugar que temos garantias que, ao passar dos anos, nenhum engraçadinho irá inserir implementações. A interface é uma forma forte de “promessa” de que aquilo é uma abstração e continuará sendo.

    É claro que toda classe abstrata é uma forma de abstração. Mas deixo claro aqui que estou falando daquele caso em específico.

    1. Kico (Henrique Lobo Weissmann)

      Oi Vinícius, que ótimo que gostou cara, valeu!

      Você falando sobre interfaces sabe o que me lembrou? Eiffel

  6. Bom post!
    A POO tá muito banalizada. Pessoal acha q é só sair criando objetos e pronto. Não se preocupam em manutenção e nada.
    Esse tipo de conceito deveria ser ensinando, ou pelo menos citado, nas aulas da faculdade.

  7. Marcelo Alves

    Parabéns pelo artigo!
    Passei pelos mesmos problemas que vc no início quando saí do desenvolvimento procedural para me ingressar na orientação a objetos.
    Já li vários livros e artigos (Patterns of Enterprise Application Architecture, DDD, UML e Patterns, etc) e o que percebo, me corrijam se estiver me equivocando, que tudo isto (padrões, boas práticas, paradigmas, …), são derivações de ‘organização’, baseadas em experiências e abstrações.

    1. Kico (Henrique Lobo Weissmann)

      Oi Marcelo, legal que tenha gostado. Valeu!

      Cara, você está certo. Eu vejo como quase uma tentativa de “padronizar” nomenclaturas para problemas comuns, exatamente como é feito com padrões de projeto.

  8. Rodrigo Vieira

    Cara, mais um parabéns pelo artigo e pelo seu blog! São simplesmente sensacionais. Já usei um deles na minha pós graduação, inclusive.

    Já escrevi isso em outros blogs, mas não me canso de dizer: é decepcionante ver que, ao mesmo tempo que estudos sobre a orientação a objetos progridem mais e mais ainda é muito fácil ver por ai código cheio de gets/sets, classes procedurais e um baita trabalho de manutenção como consequencia.

    Sou a favor de, ao invés de se dar tanta ênfase à sopa de letrinhas (os frameworks) que o profissional já trabalhou, deveria se perguntar sobre princípios da orientação a objetos, diferenças entre paradigmas, como o entrevistado modelaria um pequeno problema, enfim, se ele é capaz de escrever bons sistemas.

    Continue sempre assim!!

    1. Kico (Henrique Lobo Weissmann)

      Uau Rodrigo, que honra cara, valeu!

      Sabe, eu concordo 120% com você. Num mundo como o de hoje, com 398402384 frameworks, bibliotecas, etc, o que eu observo é o seguinte: sim, as pessoas sabem usar frameworks, mas não sabem programar. Já reparou isto?

      1. Rodrigo Vieira

        Você simplesmente resumiu o que penso e o que eu escrevi em apenas 1 linha:

        “As pessoas sabem usar frameworks, mas não sabem programar”. Ô mania essa do ser humano de querer simplificar as coisas onde não deve….parece que usando um framework da moda e umas classes procedurais, as regras de negócio serão escritas num passe de mágica.

        As vezes fico pensando que a maioria dos programadores querem simplesmente entregar qualquer coisa e ir pra casa (pois se ocorrer algum problema ou a manutenção for difícil beleza, logo estarão empregados em outro lugar, afinal, o mercado está aquecido)….sad but true.

        Ah, o texto que usei na minha pós graduação foi este:

        https://devkico.itexto.com.br/?p=172

        Grande abraço e, mais uma vez, continue sempre assim, tentando sempre escrever software de qualidade.

        1. Kico (Henrique Lobo Weissmann)

          Oi Rodrigo, valeu!

          Cara, agora faz sentido pra mim porque de repente este texto começou a receber tantos acessos! Gostou dele?

  9. Ei cara, gostei muito do artigo, vc evitou um equivoco futuro rsrs. Poo e analise oo sempre pensei que simplificar era o melhor, mas not Valew pelas dicas, d

    e sua propria experiencia, ajudou uma academica de Sis ;)

  10. RAFAEL DA COSTA CAVALCANTI

    Olá. Muito obrigado pelo artigo. Eu que estou fora do mercado a uns anos me ajudou a reavivar a memória com novas perspectivas. Vou continuar lendo as referências que citou. Abraço.

Deixe uma resposta

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Rolar para cima