O Assalto à Programação Orientada a Objetos (OOP) 🔫🧐
Um dos equívocos frequentes entre nós, programadores, é ceder muito facilmente à tentação de não colocar os comportamentos certos nos lugares certos.
Ei, caro programador e programadora ! Lembra-se da primeira vez que mergulhou no mundo da Programação Orientada a Objetos? Talvez tenha sido uma revelação, ou talvez uma ligeira confusão 😂. De qualquer maneira, é uma jornada que muitos de nós compartilhamos. Vamos voltar ao básico e redescobrir o coração da OOP.
Lembra da primeira vez que você criou uma classe, com seus atributos e métodos, e de repente seu código pareceu ganhar vida? Era como se você estivesse modelando o mundo real em seu editor de código. No entanto, ao longo do caminho, à medida que nos aprofundamos em padrões de design, arquitetura e otimizações, às vezes nos esquecemos da essência, dos fundamentos. Gostaria de iniciar relembrando os pilares.
Princípios Fundamentais 🔍
No cerne da OOP, temos os pilares: Encapsulamento, Polimorfismo, Herança e Abstração. Pode parecer básico para você, mas quantas vezes nos pegamos aplicando-os de forma quase automática, sem realmente refletir sobre o seu propósito? Quando encapsulamos, não estamos apenas "escondendo" dados; estamos definindo fronteiras, criando um contrato de como nosso objeto interage com o mundo. O polimorfismo não é apenas sobre múltiplas formas, mas sobre criar código adaptável, que pode lidar com o inesperado. Herança não é só reutilização de código; é sobre criar uma relação hierárquica significativa. E a abstração nos permite focar no que é realmente importante, descartando detalhes desnecessários.
Dados e Comportamentos 🏃🏻🎲
Para compreender profundamente a OOP e como ela se diferencia de outras metodologias de programação, é essencial entender a relação entre esses dois conceitos.
1. Dados 🎲
Dados representam as informações ou os "atributos" de um objeto. Em termos simples, são os detalhes ou as características desse objeto. Por exemplo, pense em um carro. O que vem à mente quando você tenta descrevê-lo? Talvez você pense na cor, no modelo, na marca, na velocidade máxima, entre outros. No contexto da OOP, todas essas características são os "dados" do objeto carro.
public class Carro
{
public string Marca { get; set; }
public string Modelo { get; set; }
public string Cor { get; set; }
public int VelocidadeMaxima { get; set; }
}
Nesse exemplo, Marca
, Modelo
, Cor
e VelocidadeMaxima
são os dados associados ao objeto Carro
.
2. Comportamentos 🤪
Se os dados são as características que descrevem um objeto, os comportamentos são as ações que esse objeto pode realizar. Voltando ao exemplo do carro, o que um carro faz? Ele pode acelerar, frear, ligar, desligar, etc. Essas ações que um carro pode executar são seus comportamentos.
Aqui, Acelerar
, Frear
, Ligar
e Desligar
são métodos que representam os comportamentos do objeto Carro
.
Relação Entre Dados e Comportamentos 🫶🏼
Uma das belezas da OOP é que ela nos permite encapsular dados e comportamentos juntos em uma única unidade, chamada de "objeto". Isso não é apenas uma escolha de design, mas uma maneira de refletir o mundo real em nosso código.
Pense em você mesmo. Você tem atributos (dados) como nome, idade, altura, etc., mas também tem comportamentos: pode andar, falar, pensar, etc. A OOP captura essa essência ao nos permitir modelar objetos em nosso código da mesma forma que vemos e interagimos com objetos no mundo real.
Mas como estamos refletindo a OOP nos projetos que estamos trabalhando no dia a dia? Vamos falar sobre isso.
Desmistificando a Separação de Dados e Comportamentos 🤨
Há uma crença de que separar dados e comportamentos em nossas classes é a maneira "correta" ou "mais pura" de praticar a OOP. Vamos quebrar essa noção com um exemplo:
A primeira vista, separar a representação dos dados (como vemos em Produto
) dos comportamentos associados (como em ProdutoOperations
) pode parecer uma ideia brilhante. "Cada coisa no seu lugar", pensamos 😂. Mas, ao nos aprofundarmos, percebemos que essa abordagem pode ocultar riscos e complexidades. Vamos juntos entender.
Desencapsulamento dos Domínios 🎁
A premissa fundamental da programação orientada a objetos (OOP) é a encapsulação conjunta de dados e comportamentos. No entanto, ao desassociar esses elementos, as regras de negócio, que devem ser integradas de forma coesa ao domínio, começam a se dispersar. Isso resulta em uma lógica de negócios fragmentada, o que complica o rastreamento e a manutenção do código.
Vamos considerar um exemplo de um sistema bancário:
Neste exemplo, ContaCorrente
apenas contém dados, enquanto ContaCorrenteService
contém toda a lógica. À primeira vista, pode parecer limpo. Porém, se precisarmos implementar novas regras de negócio, como taxas ou limites diferenciados para saque, o Service
pode ficar inchado e complexo. Vamos falar rapidamente dos riscos.
Orquestradores João-Faz-Tudo
Ao deslocar a lógica de negócios do domínio para os "serviços" ou "orquestradores", caímos em uma armadilha. Estes orquestradores tornam-se responsáveis por muito mais do que deveriam, contrariando o princípio de responsabilidade única.
Em sistemas complexos, podemos acabar com orquestradores que têm múltiplas responsabilidades, lidando com a lógica de várias entidades diferentes, tornando o código denso e altamente acoplado.
O Impacto Direto na Manutenção e Extensibilidade
Com regras de negócio fora do domínio e localizadas nos orquestradores, o código torna-se menos intuitivo. Além disso, ao introduzir novas funcionalidades ou mudar regras existentes, você se vê obrigado a modificar os orquestradores, o que pode levar a efeitos colaterais indesejados.
Considere uma nova regra: os clientes premium do banco podem sacar mesmo com saldo insuficiente até um limite de descoberto. Implementar isso no ContaCorrenteService
atual seria problemático, pois você teria que introduzir a lógica de verificar se um cliente é premium, qual é o seu limite de descoberto, etc. O Service
cresceria em complexidade e se tornaria mais difícil de manter.
O Risco de Violar a Encapsulação ⚠️
O encapsulamento é uma pedra angular da OOP. Ao mover a lógica de negócios para fora do domínio, estamos expondo detalhes que deveriam estar escondidos. Isso não apenas torna o código menos seguro, mas também mais propenso a erros, pois os desenvolvedores podem fazer alterações nos dados diretamente, ignorando a lógica de negócios.
Riscos associados a Classes "Saco de Dados"
As classes Saco de Dados (ou em inglês, "Data Bags" ou "Data Structures") são classes que, essencialmente, apenas armazenam dados sem fornecer qualquer funcionalidade ou comportamento específico. Em termos de estrutura, elas geralmente contêm apenas campos públicos (ou propriedades com getters e setters triviais) e nenhum método significativo.
Violação do SRP: Sem uma responsabilidade clara, é fácil adicionar funcionalidades aleatórias, contrariando o Princípio de Responsabilidade Única.
Manutenibilidade Comprometida: A lógica espalhada pelo código torna o sistema menos flexível a mudanças.
Encapsulamento Fraco: Expondo dados sem controle ou validação, elas contrariam um pilar central da OOP.
Reutilização Problemática: A ausência de comportamentos torna a reutilização mais propensa a duplicação de lógica.
Em resumo, embora pareçam simples, as classes "Saco de Dados" podem causar complexidade não intencional, tornando um sistema menos coeso e mais difícil de manter.
Orquestradores, Serviços e a Salvaguarda do Domínio 🤵♂️
Quando falamos sobre arquitetura de software, especialmente em um mundo dominado por sistemas complexos e interconectados, orquestradores e serviços muitas vezes aparecem como personagens principais no palco. No entanto, embora esses componentes sejam cruciais para garantir a operação suave de um sistema, eles não devem obscurecer ou prejudicar a essência do domínio. Vamos mergulhar profundamente no papel e no propósito destes agentes e como eles se relacionam com o domínio.
Orquestradores: Maestros do Fluxo de Trabalho
Imagine uma orquestra. Você tem vários músicos, cada um com um instrumento diferente, prontos para criar música harmoniosa. No entanto, sem um maestro para coordenar e dirigir, você teria apenas uma cacofonia de sons. Da mesma forma, em sistemas complexos, orquestradores atuam como maestros.
Os orquestradores são responsáveis por coordenar diferentes serviços, garantindo que eles trabalhem juntos de forma harmoniosa. Eles entendem a sequência, as dependências e a lógica de negócios de alto nível. Por exemplo, ao processar um pedido online, um orquestrador pode coordenar serviços como pagamento, inventário, envio e notificação ao cliente.
Mas qual é a relação dos orquestradores com o domínio?
A distinção entre domínio e orquestradores é vital para a clareza e integridade da arquitetura de software. O domínio reflete o núcleo da lógica de negócios: suas regras, restrições e relações. De forma simplificada, ele é a essência e a definição de "o que" o sistema faz em termos de regras de negócios.
Por outro lado, os orquestradores cuidam da coordenação e execução. Eles definem "como" essas regras do domínio são colocadas em prática dentro de um fluxo ou processo específico. Os orquestradores lidam com a sequência, a interação entre componentes e a forma como os dados fluem através do sistema.
O erro reside em permitir que os orquestradores se aprofundem demais na lógica de negócios, em vez de apenas orquestrar. Quando isso acontece, temos orquestradores tornando-se parte do domínio e assumindo responsabilidades que deveriam ser do próprio domínio. Esse entrelaçamento complica a manutenção e a clareza, pois separar a lógica de negócios da coordenação se torna um desafio.
Em resumo, enquanto o domínio foca no "o quê", os orquestradores focam no "como". Misturar os dois pode obscurecer a clareza da arquitetura e tornar o sistema mais difícil de evoluir e manter. Vamos falar sobre isso mais afundo em breve.
Serviços: Pontes Para Funcionalidades Especializadas 🌉
Os serviços, por outro lado, são como os músicos individuais na orquestra. Cada serviço é especializado e focado. Eles realizam tarefas específicas, como processar um pagamento ou verificar o inventário. Em uma arquitetura bem projetada, os serviços são agnósticos ao consumidor. Eles não se preocupam com quem os está chamando, seja um orquestrador, outro serviço ou até mesmo uma interface do usuário.
Entretanto, é vital garantir que os serviços não se tornem repositórios de lógica de domínio. Se começarmos a embutir regras de negócios dentro de um serviço, ele rapidamente se torna monolítico e difícil de gerenciar. Em vez disso, os serviços devem se comunicar com o domínio para tomar decisões baseadas nas regras de negócios.
UseCases e a Dança com o Domínio 💃🏼
Agora, como os casos de uso se encaixam nisso? Eles servem como um guia. Ao definir claramente o que um sistema deve fazer, os casos de uso ajudam a moldar o domínio. Eles agem como uma ponte entre os requisitos de negócios e a implementação técnica.
Na engenharia de software e sistemas, um caso de uso é uma lista de ações ou etapas de eventos que normalmente definem as interações entre uma função (conhecida na Linguagem de Modelagem Unificada como ator) e um sistema para atingir um objetivo. - Definição Geral e de Negócio.
A definição acima não traz o brilho nos olhos de nenhum programador, por isso vamos recorrer ao livro Arquitetura Limpa:
Esses casos de uso orquestram o fluxo de dados de e para as entidades e orientam essas entidades a usarem suas Regras Críticas de Negócios para atingir os objetivos do caso de uso. - Livro Clean Architecture.
Gostaria de conversar melhor sobre a frase acima retirada do livro.
Vamos dividir e entender a declaração:
"Esses casos de uso orquestram o fluxo de dados de e para as entidades..."
Um caso de uso refere-se a uma ação específica ou a um conjunto de ações que um sistema pode realizar. Por exemplo, em um sistema de comércio eletrônico, um caso de uso pode ser "Realizar Pedido". Esse caso de uso seria responsável por orquestrar várias operações, como verificar a disponibilidade de um item, processar o pagamento e atualizar o inventário.
Quando se diz que os casos de uso "orquestram o fluxo de dados de e para as entidades", significa que eles gerenciam a interação e o movimento de informações entre várias partes do sistema.
As "entidades" referem-se a objetos ou conceitos principais no domínio do negócio. No contexto do comércio eletrônico, entidades poderiam ser Cliente, Pedido, Produto, etc.
"...e orientam essas entidades a usarem suas Regras Críticas de Negócios para atingir os objetivos do caso de uso."
As "Regras Críticas de Negócios" são os princípios, restrições e lógicas que governam o funcionamento de um negócio ou sistema. Por exemplo, uma regra de negócios em nosso exemplo de comércio eletrônico pode exigir que um cliente só possa comprar um produto se ele estiver disponível em estoque.
Quando um caso de uso "orienta essas entidades a usarem suas Regras Críticas de Negócios", ele está instruindo ou guiando as entidades a aplicar essas regras específicas para alcançar o objetivo do caso de uso. No exemplo "Realizar Pedido", o caso de uso pode guiar a entidade Pedido a aplicar regras de negócios para verificar a disponibilidade do produto e, se disponível, prosseguir com outras etapas, como dedução do estoque e processamento de pagamento.
Ao desenvolver sistemas, é crucial referenciar regularmente esses casos de uso para garantir que o domínio permaneça puro. Sem essa referência, há o risco de "vazamento de domínio", onde decisões técnicas começam a distorcer ou diluir as regras de negócios.
Exemplo 1: Em uma loja online, um caso de uso pode ser "Comprar Produto". Este caso de uso incluiria todas as etapas que o usuário (ator) tomaria para pesquisar um produto, adicioná-lo ao carrinho, fornecer informações de envio e pagamento e, finalmente, confirmar a compra.
Exemplo 2: Em um banco, um caso de uso poderia ser "Transferir Dinheiro". O usuário, neste caso, passaria por etapas de selecionar uma conta de origem, especificar o montante, fornecer detalhes da conta de destino e confirmar a transferência.
Relação entre UseCases e Regras de Domínio
A confusão surge quando as linhas entre UseCases e as regras do domínio começam a se misturar. UseCases devem lidar com a coordenação e sequência das ações, enquanto as regras de domínio se preocupam com a lógica de negócios subjacente e como os dados são tratados e manipulados.
No exemplo da "Transferir Dinheiro", o UseCase deve coordenar a sequência de eventos (selecionar contas, especificar montantes, confirmar, etc.), mas as regras exatas de como o dinheiro é transferido, as verificações de saldo, as taxas aplicadas, etc., residem no domínio e devem ser encapsuladas dentro das entidades e agregados relevantes, como ContaBancaria
.
Evitando o Vazamento de Regras de Domínio para UseCases
O "vazamento" ocorre quando começamos a mover a lógica de negócios e regras de domínio do domínio para os UseCases. Isso é problemático porque:
Viola a Coesão: O sistema perde sua coesão, e a lógica de negócios se torna espalhada, tornando-se difícil de manter e entender.
Testabilidade: UseCases sobrecarregados com regras de negócios se tornam mais difíceis de testar, pois você terá que mockar e configurar muito mais condições.
Reutilização: Uma vez que a lógica de negócios é misturada com a coordenação de ações, torna-se quase impossível reutilizá-la em outros contextos sem duplicação.
Vamos começar com um exemplo prático para ilustrar o vazamento de regras de domínio para UseCases. Vamos imaginar um cenário simples de um sistema de pedidos em que temos a necessidade de aplicar um desconto ao pedido com base em critérios específicos.
Neste exemplo, podemos ver que a lógica para aplicar o desconto, que é claramente uma regra de negócios, foi colocada no UseCase. Isso é um vazamento, porque as regras de negócios que deveriam estar contidas no domínio estão agora no UseCase.
Para evitar o vazamento:
Clareza nos Requisitos: Ao escrever ou revisar UseCases, certifique-se de que ele descreve a sequência de ações e interações, e não se aprofunda na lógica de negócios.
Domínio Rico: Promova um domínio rico, onde entidades e agregados têm comportamentos, e não apenas dados. Eles devem encapsular as regras de negócios e lógicas relacionadas.
Delegar Responsabilidades: Quando estiver implementando um UseCase, ao encontrar uma necessidade de aplicar uma regra de negócios, delegue essa responsabilidade ao domínio.
Uma Pequena Reflexão 😄
Podemos ser levados a colocar regras de negócios no UseCase por algumas razões:
Falta de Entendimento: Podemos não entender claramente a distinção entre regras de domínio e a coordenação de ações que um UseCase deve realizar.
Simplicidade Imediata: Em um primeiro olhar, pode parecer mais simples colocar a lógica diretamente no UseCase. Afinal, é "apenas uma linha de código", certo? Mas à medida que o sistema cresce, essa abordagem torna-se rapidamente insustentável.
Agora vamos corrigir nosso exemplo:
Por exemplo, ao implementar o UseCase "Transferir Dinheiro", ao invés de codificar a lógica de verificação de saldo e aplicação de taxas no UseCase, você simplesmente chamaria um método na entidade ContaBancaria
, como contaOrigem.transferirPara(contaDestino, montante)
, e deixaria a ContaBancaria
cuidar dos detalhes.
Dessa forma, o sistema mantém a lógica de negócios centralizada nas entidades (promovendo coesão e reutilização) enquanto permite que os UseCases gerenciem a complexidade de coordenar várias entidades e operações para atingir um objetivo específico.
O Assalto Silencioso ao Encapsulamento! 🚨🔫
Esse pilar muitas vezes é visto como uma espécie de "boa prática" 😂, mas o que é alarmante é que, ao longo do tempo, esse princípio vital tem sido mal interpretado ou, em alguns casos, negligenciado por completo.
Entender o encapsulamento como simplesmente "esconder" dados não é apenas superficial, mas é também um entendimento errado. O encapsulamento não é sobre esconder, mas sobre proteger. Imagine o DNA de uma célula: ele não é "escondido" dentro do núcleo só porque sim. Ele é guardado ali para protegê-lo de influências externas prejudiciais. Da mesma forma, o encapsulamento em OOP protege os dados de serem manipulados de maneiras que possam prejudicar a integridade do programa.
Vamos explorar um exemplo comum em C# para ilustrar o ponto:
Nesta simples classe Carro
, todos os membros são públicos. Isso significa que qualquer código de fora pode modificar diretamente o Modelo
, Ano
e Preco
sem qualquer restrição.
Veja os problemas aqui? Podemos facilmente atribuir um ano negativo e um preço negativo para o carro, o que é logicamente incorreto. O encapsulamento bem feito poderia nos proteger de tais inconsistências.
Agora, você pode se perguntar: por que isso acontece? Por que alguns desenvolvedores deixam de lado o encapsulamento adequado?
Parte do problema pode ser a pressa. Em ambientes de desenvolvimento ágil, onde a entrega rápida é frequentemente priorizada, pode haver uma inclinação para escrever o código "que apenas funciona", sem considerar adequadamente a estrutura ou o design. Em outros casos, pode ser uma falta de compreensão ou apreciação pela profundidade do conceito.
Outra parte do problema é a educação. Muitas vezes, os conceitos da OOP são ensinados apenas de forma muito teórica. Sem exemplos práticos relevantes, o encapsulamento pode parecer uma abstração desnecessária. "Por que eu deveria me preocupar em esconder isso?" pode ser uma pergunta comum.
Mas, ao pensar assim, perdemos o verdadeiro propósito. Sem encapsulamento, nosso software se torna frágil. Eles se tornam suscetíveis a erros, à medida que mais e mais código começa a interagir com partes que deveriam ser protegidas. Isso não é apenas uma questão teórica - pode levar a bugs reais, falhas de sistema e horas intermináveis de depuração.
E tem mais. A falta de encapsulamento adequado torna o código menos reutilizável. Quando a classe Carro
é projetada sem proteções adequadas, ela se torna uma entidade vulnerável em nosso código. Assim, qualquer parte do código, em qualquer lugar do sistema, pode alterar seu estado, muitas vezes de maneiras não previstas ou não intencionais. Isso é especialmente problemático porque uma classe não deve apenas representar dados, mas também garantir que esses dados permaneçam consistentes e válidos em todos os momentos.
Por outro lado, um bom encapsulamento permite que os desenvolvedores modifiquem a lógica interna de uma classe ou componente sem afetar os consumidores dessa classe. Ele age como um contrato: "Não se preocupe com o que acontece por dentro. Eu prometo te fornecer o que você espera, contanto que você interaja comigo nos termos que estabeleci". Ao encapsular corretamente uma classe, você está fornecendo uma "interface" clara para outros desenvolvedores - uma espécie de "manual de instruções".
Quando nos afastamos desses princípios, estamos não apenas assaltando a integridade da OOP, mas comprometendo a qualidade do software que produzimos. Estamos abrindo mão da robustez, da manutenção e, em muitos casos, da segurança.
Vamos tomar um novo olhar sobre o exemplo dado e refatorá-lo de forma apropriada, priorizando o encapsulamento correto.
Refatorando com Encapsulamento em Mente 🧠
Quando um programador tentar instanciar um Carro
e atribuir valores inválidos a esses membros, ele receberá um erro. Esse erro atua como um guia, dizendo ao desenvolvedor que algo não está certo e deve ser corrigido.
var meuCarro = new Carro();
meuCarro.Modelo = "XYZ";
meuCarro.Ano = -1500; // Erro! Valor fora do intervalo aceitável.
meuCarro.Preco = -10000; // Erro! Valor negativo não é aceito.
Agora, o encapsulamento serve como uma muralha, protegendo a lógica interna da classe e garantindo que ela permaneça consistente e lógica. E isso, em essência, é o que a OOP se esforça para alcançar: criar blocos de construção robustos e reutilizáveis que possam ser combinados de maneiras complexas para construir sistemas maiores e mais complexos, sem comprometer a integridade ou funcionalidade desses blocos.
Além disso, ao focar em encapsulamento apropriado, estamos também tornando nosso código mais manutenível. No futuro, se precisarmos mudar como o preço é determinado ou se precisarmos adicionar lógica adicional para o modelo do carro, podemos fazer isso sem afetar o código que usa nossa classe Carro.
Pode estar pensando: “Mas isso é muito verboso!“ Realmente! Mas podemos utilizar no Csharp uma sintaxe menos verbosa:
Nada te impede de utilizar a biblioteca popular para validações em C# , FluentValidation. Ela permite definir regras de validação de forma fluente e declarativa. Mas cuidado para não ficar viciado nela! 😂
O Assalto à OOP! 🔫
Quando separamos dados e comportamentos em nossa codificação, estamos comprometendo um dos pilares fundamentais da programação orientada a objetos: o encapsulamento. A OOP, em sua essência, é sobre a união de dados e os comportamentos que operam sobre esses dados em uma única entidade coesa - o objeto.
No cerne deste "assalto" está uma desconexão entre os princípios fundamentais da orientação a objetos e as práticas diárias adotadas por desenvolvedores. Enquanto a OOP, em sua essência, promove clareza, coesão e modularidade, esses desvios muitas vezes resultam em código que é tudo menos isso. Muitos projetos de software tendem ao lado de separar dados e comportamentos. Como se fosse um assalto aos pilares. É como falar: “Passe já todos esses comportamentos!“. E isso pode não parecer tão prejudicial no inicio… mas ao longo do tempo isso se torna um baita problema!
Vou falar um pouco sobre algumas razões pelas quais a separação de dados e comportamentos pode ser considerada um "assalto" à verdadeira natureza da OOP:
Violação do Encapsulamento: Como discutimos muito, ao separar os comportamentos dos dados, você está basicamente tornando todos os dados públicos, permitindo que sejam alterados de qualquer lugar no código. Isso viola o princípio do encapsulamento, que busca proteger o estado interno de um objeto.
Perda de Coesão: Em OOP, buscamos criar classes coesas, onde cada classe tem uma responsabilidade clara e bem definida. Quando separamos os comportamentos dos dados, as classes se tornam menos coesas, e é menos claro qual é a sua responsabilidade real.
Aumento de Acoplamento: Quando comportamentos são separados dos dados, as chances de um acoplamento indesejado entre classes aumentam. Classes que contêm comportamentos precisam conhecer a estrutura interna das classes de dados, o que torna o sistema mais frágil a mudanças.
Dificuldades de Manutenção: Suponha que você queira mudar a forma como um certo dado é armazenado ou calculado. Se os comportamentos estiverem em uma classe separada, você terá que revisar ambas as classes, e talvez muitas outras, para garantir que todas as interações entre elas ainda funcionem como esperado.
Testabilidade Comprometida: Classes que são puramente dados ou puramente comportamento são mais difíceis de serem testadas isoladamente. Na OOP, um objeto bem definido pode ser facilmente instanciado e testado em várias condições. No entanto, se o comportamento for separado dos dados, os testes podem requerer a criação de muitos mocks, tornando-se menos claros e mais propensos a erros.
Contraintuitivo para Modelagem do Domínio: Quando modelamos um software, frequentemente pensamos em termos de entidades do mundo real e suas interações. Por exemplo, um "Carro" pode "Acelerar" ou "Frear". É natural combinar esses conceitos em uma única classe
Carro
que possui métodosAcelerar()
eFrear()
. Separar os dados e comportamentos aqui iria contra nossa compreensão intuitiva do problema.
Reverter o "assalto" não é uma tarefa fácil. Requer uma reeducação, uma reavaliação das prioridades e, mais importante, uma compreensão e apreciação renovadas dos princípios fundamentais da OOP. Agora seria interessante ser sincero sobre o cenário atual da engenharia de software!
O Ponto de Equilíbrio
É importante buscar o equilíbrio. Nem todas as classes em nossa aplicação precisam ser ricas em comportamentos. Na realidade, há cenários em que ter classes simples, ou "sacos de dados", é não apenas aceitável, mas também apropriado.
Vamos pegar, por exemplo, os DTOs (Data Transfer Objects). Eles são frequentemente usados em camadas de aplicação para transferir dados entre subsistemas, especialmente quando interagimos com interfaces, bancos de dados ou serviços externos. A simplicidade e a previsibilidade dos DTOs são suas maiores forças. Eles são claros, diretos e, na maioria das vezes, não têm qualquer lógica associada. Estão ali para transportar dados, e fazem isso muito bem.
Há uma série de projetos ou serviços onde o foco principal é o trânsito, armazenamento ou apresentação de dados, e as regras de negócios ou comportamentos sofisticados não são necessários. Vamos examinar alguns cenários em que isso acontece:
Serviços CRUD Básicos: Alguns serviços são puramente destinados a criar, ler, atualizar e excluir registros de um banco de dados ou outra fonte de dados. Em tais casos, a complexidade em torno da lógica de negócios é mínima, e a principal preocupação é garantir o acesso eficiente aos dados.
Proxies e Gateways: Estes são serviços intermediários que simplesmente repassam solicitações de um ponto a outro. Eles podem manipular dados em trânsito, mas geralmente não impõem regras de negócios.
Cachês e Armazenamentos Temporários: Sistemas que atuam como caches ou armazenamentos temporários estão mais preocupados com a velocidade e a eficiência do armazenamento e recuperação de dados do que com a lógica de negócios.
Transformadores de Dados: Existem serviços cuja principal tarefa é transformar dados de um formato para outro (por exemplo, XML para JSON ou vice-versa). A lógica aqui está na transformação, e não nas regras de negócios.
Aplicações Front-end Simples: Algumas aplicações de apresentação ou dashboards apenas exibem dados provenientes de uma API ou banco de dados. O processamento real e a lógica de negócios estão contidos em outro lugar, e a aplicação front-end serve principalmente como uma camada de visualização.
Logs e Monitoramento: Serviços que coletam, armazenam e talvez visualizem logs e métricas geralmente não requerem lógica de negócios complexa. Eles estão preocupados em capturar e armazenar eventos conforme ocorrem.
No entanto, quando começamos a entrar no território dos sistemas corporativos - aqueles sistemas grandes, que gerenciam operações críticas e cujos dados precisam ser manuseados com a maior precisão - a história muda. Estes sistemas muitas vezes têm regras complexas e nuances que precisam ser cuidadosamente orquestradas. Em tais cenários, confiar em classes que são apenas "sacos de dados" pode se tornar arriscado. Isso porque eles não oferecem a capacidade de garantir a integridade dos dados por si só.
Aqui, a importância dos comportamentos se destaca. Os comportamentos garantem que as entidades se mantenham em um estado válido, de acordo com as regras de negócio.
Desvios da Essência… 😕
A jornada da programação orientada a objetos (OOP) tem sido sinuosa e repleta de reviravoltas. Desde sua concepção até sua aplicação no mundo real, muitos têm interpretado e reinventado a OOP de maneiras que nem sempre alinham com sua essência. Vamos mergulhar em algumas das razões pelas quais isso está acontecendo. Não vou citar muitas para não deixar o post mais longo do que já está 😅.
Uma grande razão para essa derivação é o ritmo acelerado da indústria de tecnologia. Novas linguagens, frameworks e ferramentas emergem quase que diariamente, cada uma com suas abstrações e padrões. Para um desenvolvedor, manter-se atualizado é vital, mas também é fácil ser atraído pelo "novo" e “performatico“, esquecendo-se das bases e princípios fundamentais. Em meio a essa avalanche de novidades, os princípios da OOP podem parecer um pouco "antigos" ou "tradicionais" demais para alguns.
Outra consideração é a pressão constante por produtividade e entrega rápida. Em muitos ambientes de trabalho, o foco está em entregar rapidamente, em vez de entregar com qualidade e de acordo com os princípios bem estabelecidos. Nesse cenário, é tentador pegar atalhos. Ao invés de pensar cuidadosamente sobre o design e a coesão de um objeto, é mais fácil simplesmente dividir os dados e os comportamentos, por exemplo. O famoso"Funciona" torna-se um mantra, mesmo que depois se transforme em uma dívida técnica massiva que a equipe terá que enfrentar mais tarde.
Além disso, a própria natureza colaborativa da programação pode, paradoxalmente, levar a problemas. Em equipes grandes, nem todos terão o mesmo nível de compreensão ou apreciação pela OOP. Se um membro da equipe não está totalmente a bordo ou simplesmente não entende, ele pode começar a escrever código que desvia da orientação a objetos pura. Sem revisões de código rigorosas ou padrões claros estabelecidos, essas divergências podem se infiltrar no código base e se tornar a norma, em vez da exceção.
Há também uma questão cultural mais ampla em jogo. Vivemos em uma era de gratificação instantânea. Queremos resultados imediatos. E com tantos recursos à nossa disposição, muitas vezes sentimos que devemos usar todos eles. Isso pode levar a um overengineering massivo, onde uma solução simples baseada em sólidos princípios de OOP é ofuscada por camadas e camadas de complexidade. Às vezes, é um caso clássico de não ver a floresta por causa das árvores. Ficamos tão envolvidos nas minúcias do código que esquecemos o panorama geral: criar software que resolva problemas reais de maneira eficiente e manutenível.
Além disso, a própria educação em ciência da computação pode ser parcialmente culpada. Muitos cursos enfatizam algoritmos e estruturas de dados, mas não necessariamente os princípios e práticas de bom design de software. Um programador pode sair da faculdade ou curso sabendo como criar uma árvore binária, mas não necessariamente como projetar um sistema orientado a objetos coeso e bem estruturado.
O que fazer?
A conscientização é o primeiro passo. Em um mundo onde a velocidade muitas vezes supera a sustentabilidade, é essencial pausar e refletir sobre as abordagens que estamos adotando. Devemos nos perguntar: "Estou realmente usando a OOP para o seu potencial máximo? Ou estou apenas usando seus trajes, enquanto negligencio sua essência?".
Cultivar um entendimento genuíno e uma apreciação pela programação orientada a objetos exige mais do que apenas saber como criar uma classe ou entender a herança. É preciso entender a filosofia subjacente, o desejo de modelar o mundo real de forma lógica e intuitiva, de modo que o software possa refletir a realidade tão naturalmente quanto possível.
Além disso, no ambiente de trabalho, é crucial criar uma cultura onde a qualidade do design não é sacrificada em prol da entrega rápida. Isso pode envolver discussões difíceis e possivelmente impopulares sobre prazos e escopo, mas a longo prazo, um código bem projetado e orientado a objetos economizará tempo, dinheiro e muita dor de cabeça.
A vantagem da OOP é que, quando bem implementada, ela pode tornar o código mais legível, manutenível e adaptável. As soluções são modeladas de forma mais próxima ao domínio do problema, tornando-as mais intuitivas para desenvolvedores e stakeholders. Em contraste, quando nos afastamos dos princípios fundamentais da OOP, nos arriscamos a perder esses benefícios.
Em vez de ser vista como ultrapassada ou restritiva, a OOP deve ser valorizada como a poderosa ferramenta que é, capaz de trazer clareza ao caos e ordenar sistemas complexos favorecendo a fácil compreensão do domínio.
Assim, a cada linha de código que escrevemos, a cada objeto que modelamos, devemos nos esforçar para honrar a visão original da OOP, enquanto também permanecemos abertos a inovações e mudanças. E, acima de tudo, devemos lembrar que, no centro de qualquer tecnologia, está a humanidade.
Esse post foi inspirado por Leonardo Leitão, da Cod3r, em seu vídeo no Youtube, link no final. Vale muito a pena conferir também!
Fico por aqui neste post! Obrigado por ler até o final, se este post foi útil, compartilhe! Até o próximo! 😄