Artigos

Princípios básicos de orientação a objetos

Quando se fala em modelar uma aplicação se pensa logo em padrões de projeto. A famigerada gangue dos quatro, em seu livro de Design Patterns descreve um catálogo de boas práticas “simples e elegantes soluções para problemas de design orientado a objetos”. Muito poderoso. O problema é que muitos desenvolvedores, como sempre, não entenderam muito bem os conceitos e saíram, e ainda saem, aplicando design patterns como se não houvesse amanhã, gerando, inclusive o que chamamos de anti-patterns.

Dessa forma, problemas simples foram resolvidos com soluções extremamente complicadas e rebuscadas, gerando, mesmo com a melhor das intenções, o que chamamos de over engineering e overdesign.

Outro problema no design de aplicações modernas é que, assim como é possível desenvolver software com excesso de design, também é possível desenvolver sem design nenhum. Qualquer um pode escrever código que funciona agora. Nem todos tem a perícia de escrever código que funcionará sempre. Sua aplicação deve funcionar agora e estar preparada para mudar para sempre. A forma como o código seja fácil de mudar revela a qualidade do programador.

De um lado temos programadores criando soluções mirabolantes para problemas simples. De outro, temos programadores ignorando totalmente práticas de design, causando débitos técnicos e deixando o software mais rígido e mais frágil. Problemas que poderiam ser resolvidos com uma palavra: simplicidade. Precisamos retomar princípios básicos.

Mensagens

Orientação a objetos é sobre troca de mensagens entre objetos. Entende-se por mensagem, uma chamada de método. Quando se pergunta a um objeto user se ele pode dizer seu nome, se está mandando uma mensagem a user.

Essa troca de mensagens pode ocorrer de várias formas: um objeto pode responder a uma mensagem, pode delegar uma mensagem a outro objeto ou pode herdar a mensagem.

Herança é sobre delegação de mensagens através de uma hierarquia de classes, com o ônus de se manter um acoplamento entre as classes pertencentes a essa hierarquia.

Dependências

Orientação a objetos é sobre gerenciar dependências. Um objeto depende de outro se, quando um mudar, o outro precisar ser alterado também. Isso acontece quando um objeto conhece demais detalhes de implementação de outros objetos e isso pode se dar das seguintes formas:

  • Um objeto conhece o nome de outra classe.
  • Um objeto conhece o nome da mensagem que ele deseja enviar a outro.
  • Conhece os argumentos que a mensagem necessita.
  • Conhece a ordem dos argumentos da mensagem.

Quanto mais as classes conhecem detalhes de implementação de outras, maior é o acoplamento entre elas. O grande desafio do design é administrar esse acoplamento e evitar que mudanças em um componente afete outros.

Lei de Demeter

Resumidamente, a Lei de Demeter prega o seguinte: somente fale com o seu vizinho imediato. Violar esta lei implica em aumentar o acoplamento entre componentes.

No exemplo acima, o código expõe detalhes demais de outro código. Isso pode ser considerado um vazamento de abstração. Isso poderia ser corrigido da seguinte maneira:

Na refatoração acima, escondemos detalhes de implementação através de uma abstração que é o método bicycle_wheel_rotate. Dessa forma, enviamos uma mensagem simples ao objeto customer, ao invés de propagá-la em uma cadeia de objetos.

DRY

DRY é um acrônimo para Don’t Repeat Yourself, que significa Não Se Repita. Sabemos que repetição de código é um problema e pode ocasionar muitos bugs. No entanto, isso pode ser levado ao extremo por muitos programadores. Em alguns cenários é preferível duplicação a se criar uma abstração errada. 

Se o código está duplicado, mas irá mudar, o custo da duplicação é pequeno. Agora, remover a duplicação criando um abstração muito cedo, pode ser mais caro. Criar abstrações cedo demais, sem compreender o domínio do problema deixa o software com uma complexidade desnecessária. Por exemplo, extrair um código para uma superclasse, onde ele será compartilhado para suas subclasses. Quando os requisitos mudarem, outro desenvolvedor pode adicionar algum parâmetro novo naquele código, sendo necessário criar lógica que funcione para cada subclasse. O que era pra ser uma abstração, agora tem comportamentos diferentes dependendo do caso.

A herança pode ser usada de maneira mais inteligente:

Em Ruby, tudo é objeto, inclusive o nil, que é representado pela NilClass. Toda classe em Ruby é subclasse de Object que implementa vários métodos inclusive o método nil? que retorna false. A NilClass sobrescreve o método nil? retornando true, enquanto as demais classes herdam diretamente a implementação de Object.

Quando invocamos o método nil? a partir de uma string, por exemplo, estamos enviando uma mensagem a um objeto String que irá se propagar por toda a hierarquia de classes até chegar a Object. Aqui, vemos a herança sendo utilizada como delegação de mensagens ao invés de um simples compartilhamento de código.

Outros Princípios

A classe hipotética Parser é responsável por fazer parseamento de arquivos para um determinado formato. Seu nome não nos diz muita coisa. Ela faz parse de que? Podemos perceber que o segundo parâmetro do construtor é uma flag que pode significar o tipo de arquivo. Bem, é provável que ela faça parseamento de outros tipos de arquivos. E se passarmos um arquivo json? Será que funciona? Não confiamos na interface dessa classe, precisamos abri-la.

Analisando o método parse percebemos que a classe faz apenas parseamento de arquivos json e xml. A classe Parser possui uma série de problemas. Ela não é especializada. Tem mais de um propósito. Ela conhece detalhes demais de implementação de parseamento de arquivos json e xml. Ela tem a tendência de se tornar cada vez mais complexa, caso um desenvolvedor decida adicionar novos formatos de arquivo. Vejamos como melhorá-la.

Extraímos duas classes novas a partir da classe Parser: XmlParser e JsonParser especializadas em fazer parseamento de seus respectivos tipos de arquivos. A classe Parser agora é uma espécie de contêiner que recebe instâncias de classes que implementam o método parse, através de injeção de dependência.

Perceba que o método parse da classe Parser, executa o método do objeto passado no construtor. Então, todo objeto passado no construtor deve implementar o método parse. Isso é um contrato entre as classes. Isso é polimorfismo

Em linguagens estáticas, podemos conseguir isso através de herança, usando o conceito de interfaces. Em Ruby, tal conceito não existe. Os contratos são feitos através de Duck Typing.

A classe Parser, que antes dependia de código de implementação de parseamento de arquivos, agora depende de abstrações. O que ocorreu foi uma inversão de controle, através de injeção de dependência no construtor de Parser.

Agora, e se precisarmos criar código de parseamento para outro formato de arquivo?

Criamos uma outra classe especializada em realizar parser de outro formato. Dessa forma, a classe Parser fica fechada para modificação e aberta para extensão. Esse é o princípio aberto/fechado.

Perceba que, na primeira versão da classe Parser, o método parse possuía lógica. Basicamente, ele perguntava qual o tipo de arquivo para tomar uma decisão.

Isso é um code smell. Em OO, não devemos ficar perguntado coisas, devemos apenas enviar a mensagem. Quando perguntamos, é sinal de baixa coesão e alto acoplamento. Significa que sabemos detalhes demais. Ao invés de perguntar, devemos apenas dizer. É isso que a nova versão do método parse faz.

Dizemos ao objeto parser o que queremos dele, através do envio de uma mensagem, com a invocação do método parse. Esse é o princípio tell, don’t ask.

Conclusão

Com uma simples refatoração de uma única classe, aplicamos vários princípios básicos de OO. Não é ciência de foguete, mas exige prática. Antes de sairmos por aí aplicando DDD ou design patterns complexos precisamos entender aspectos básicos de design. É o que liga.

Até a próxima.

 

Um comentário

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *