Orientacao A Objetos - Orientação a Objetos (2)
Orientação a Objetos (2)

Começando pelo jeito que funciona na prática

Você escreve classes que modelam coisas do domínio do problema, instancia objetos, e deixa o polimorfismo e o encapsulamento resolventem a complexidade. Orientação a objetos não é uma bala de prata, mas quando aplicada corretamente, reduz significativamente o custo de manutenção em sistemas que evoluem por anos. A parte mais difícil não é entender os conceitos, é saber quando usar cada um e quando deixar de lado.

O que é orientação a objetos na realidade

A ideia central é organizar código em torno de entidades que combinam estado e comportamento. Uma classe define um molde, e uma instância é um objeto concreto com dados próprios. Os quatro pilares são herança, polimorfismo, encapsulamento e abstração, mas na prática você vai usar apenas alguns deles na maior parte do tempo. Herança é o que mais gera problemas se usada sem critério. Já tive um projeto onde a equipe inteira estava estendendo uma classe base genérica chamada Entidade para quase tudo: usuários, pedidos, produtos, logs. O resultado foi uma hierarquia com nove níveis de profundidade que ninguém conseguia entender. A correção foi refatorar para composição, onde Entidade vira um componente inserido nos objetos que realmente precisam dele, e não um ancestral obrigatório. Isso reduziu o acoplamento e mudou o tempo de deploy de cerca de 40 minutos para 8 minutos no ambiente de staging.

Encapsulamento: o que realmente importa

Encapsulamento serve para proteger invariantes. Se uma classe tem um campo que precisa manter uma relação específica com outro campo, exponha métodos que mantenham essa relação, não propriedades públicas. O padrão getter/setter do Java é útil em alguns contextos, mas em muitos casos ele é um cheiro de código que sinaliza que a classe está expondo seu estado interno demais. Um exemplo concreto: uma classe que representa uma transação financeira. Se você permite que qualquer código modifique o valor diretamente, alguém pode esquecer de atualizar o campo data_ultima_alteracao. Colocando a modificação dentro de um método como alterarValor(), você garante que todas as regras sejam executadas. Isso parece óbvio até você ver um bug produtivo causado por exatamente esse problema.

Herança versus composição na prática

A regra de ouro que todo mundo cite mas poucos seguem é: prefira composição a herança. A herança cria um acoplamento forte e rígido. Se você muda a classe pai, todas as filhas podem quebrar sem aviso. Composição permite trocar implementações em tempo de execução e testar unidades isoladamente. Um caso que encontrei recentemente envolvia um sistema de notificação. A equipe tinha criado uma classe base Notificador com métodos para email, SMS e push. Todas as subclasses herdavam dela. Quando precisaram adicionar notificação por WhatsApp, tiveram que modificar a classe base, o que quebrou três subclasses que não usavam o novo método. A solução foi transformar Notificador em uma interface e cada estratégia de envio em uma classe separada que o cliente escolhe conforme a necessidade.

Polimorfismo: onde ele realmente brilha

Polimorfismo permite que código trate diferentes tipos de forma uniforme. O padrão Strategy é um dos usos mais práticos. Em vez de um grande if/else que decide qual lógica executar baseada no tipo do objeto, você define uma interface e deixa cada implementação cuidar da sua própria regra. Isso também facilita testes, porque você injeta mocks sem depender de frameworks de reflexão ou casting. O Side Effect é que todo mundo tende a abusar do polimorfismo em lugares onde uma simples função pura resolveria. Se você tem três valores de enumeração e quer comportamento diferente para cada um, um switch ou um mapa de funções costuma ser mais legível do que criar três classes polimórficas.

👉 Clique no botão abaixo para saber mais sobre o assunto!

Abstração: o que esquecem de mencionar

Abstração não é sobre criar interfaces genéricas demais. É sobre esconder complexidade desnecessária de quem consome sua classe. Uma boa abstração tem um contrato claro: o que o objeto faz, não como ele faz. Se alguém precisa saber dos detalhes de implementação para usar sua classe, a abstração está ruim. Um erro comum é criar uma interface ICacheManager com dezenas de métodos porque vários serviços usam o cache de formas diferentes. A interface deveria ter apenas o que é essencial para o contrato básico. Métodos específicos ficam em interfaces derivadas ou em classes concretas que o consumidor escolhe explicitamente.

Dicas que vêm de ter visto código quebrado

Sempre pense no consumidor da sua classe antes de escrever. Se a classe for difícil de usar corretamente, alguém vai usar errado. Um construtor com muitos parâmetros opcionais é um sinal de que talvez você precise de um Builder ou de uma classe de configuração separada. Evite getters que expõem coleções mutáveis. Retorne uma cópia ou uma visão imutável. Uma vez perdi duas horas rastreando um bug onde um objeto em outra parte do sistema estava modificando uma lista interna de outra classe porque o getter retornava a referência direta.

Documente invariantes, não óbvios. Escreva comentários explicando por que uma classe existe e quais condições precisam ser verdadeiras, não o que o código faz (isso já está no código). Um exemplo de bom comentário: "Esta classe garante que apenas uma instância de cada tipo de relatório seja gerada simultaneamente, mesmo em thread concorrente."

Limitações que ninguém gosta de admitir

Orientação a objetos tem custos. Objetos criam overhead de memória e de garbage collection. Em sistemas de alto desempenho como jogos ou processamento de fluxo de dados massivo, objetos podem ser um gargalo. Nesses casos, estruturas orientadas a dados ou abordagens baseadas em arrays e índices funcionam melhor. Outro ponto fraco é a dificuldade de raciocínio sobre estado mutável em contextos concorrentes. Sem cuidado, dois objetos compartilham estado e você entra no terreno de race conditions e deadlocks. Se o seu problema é fundamentalmente paralelo ou assíncrono, considere usar um paradigma funcional como complemento, mesmo que a base do sistema seja orientada a objetos.

Muitas vezes a solução ideal é híbrida. Usar objetos para modelar o domínio, mas funções puras para a lógica de transformação de dados. Isso mantém a organização que a OO oferece sem arrastar todos os seus problemas junto.

O que fazer para começar de verdade

Pegue um problema simples do dia a dia e modele classes para ele. Comece com entidades básicas, adicione comportamentos gradualmente e refatore quando perceber que algo não está encaixando. Ler código de projetos open source bem estruturados também ajuda, mas foque em projetos que você consegue entender em nível de detalhes, não apenas em high-level architecture. Pratique com exercícios que forcem decisões de design. Implementar um sistema de filas com diferentes políticas de escalonamento, um motor de regras simples, ou um simulador básico de fila de atendimento são bons exemplos. O importante é sentir o peso das escolhas de design e como cada decisão afeta o resto do sistema.