POO – Boas práticas

Olá a todos, este é meu primeiro artigo na UC, nele falarei sobre boas práticas de OO que com certeza todo desenvolvedor deveria levar em consideração, seja ele iniciante ou veterano. De fato, já existem centenas de livros e sites por ai falando sobre este assunto, então tentarei focar nos pontos mais importantes. Vamos lá!

Bom, se você já deu de cara com um código que mais lembra uma obra filosófica pela complexidade e capacidade exigida do programador para desvendar porque raios alguém fez aquilo, provavelmente deve estar dizendo a si mesmo: O que há de errado comigo? Não tomei café suficiente? Ou talvez, espere um pouco em todos esses anos nessa indústria vital essa é a primeira vez que isso me acontece.

O que fazer?

Brincadeiras a parte, desenvolver bons códigos vai além de mera organização ou anos de experiência, afinal você pode ter aprendido na faculdade, no trabalho ou no curso que classes devem ser coesas, variáveis devem ter nomes intuitivos e a fazer uso de um dos padrões de projeto já existentes ao invés de criar o seu, no entanto, sabemos que a realidade em alguns casos é outra. Uma gambiarra aqui, uma repetição de código ali, abraços e boa sorte para o próximo que colocar as mãos nisso. Este é um dos grandes problemas, pois muitas vezes a magia pode se virar contra o próprio feiticeiro e você acabar fazendo hora extra ou trabalhando no final de semana para corrigir aquilo.

Portanto, mesmo que não seja possível alterar um código arcaico e enraizado naquele projeto, já fará uma grande diferença partir de algum ponto fazendo o que é certo, ao menos para fins de manutenção posterior. Neste artigo falarei sobre uma das cartas na manga que podemos utilizar e darei continuidade em artigos posteriores, pois o assunto é extenso. Então vamos lá!

Aplique um ou mais dos cinco princípios S.O.L.I.D. sempre que possível

S-SRP, O-OCP, L-LSP, I-ISP, D-DIP, essas seriam as iniciais das siglas de cada princípio, sim eu sei, não tem problema se você não decorou, eu também nunca memorizei isso, vamos nos ater a essência, sem formalidades.

SRP (Single Responsibility Principle) ou Princípio da Responsabilidade Única

Tem uma classe com muitos métodos diferentes? Constantemente sendo modificada e que a cada dia aumenta mais e mais? Pare agora mesmo de alimentar este monstro, isso indica falta de coesão, ou seja, uma classe assumindo diferentes responsabilidades e provavelmente com bastante acoplamento com outras classes.

Dividir para conquistar

Divida a classe em outras classes menores e reduza as dependências. Parece até meio óbvia a solução, mas dependendo do número de classes que você precisa gerenciar em tempo de desenvolvimento, é algo que passa despercebido. A ideia é que ao final essas classes se tornem estáveis e seja possível reutilizá-las com pouca ou nenhuma modificação.

Acoplamento

Em relação ao acoplamento, quando se tratar de um projeto de médio/grande porte ao invés de fazer referência a uma determinada classe em diferentes partes do código podemos agrupar essas referências em uma interface, e implementar esta interface seria o mesmo que dizer, estou garantindo que farei uso de tais métodos da interface que possui acoplamento somente com as classes X, Y, Z. Se a interface for simples e bem definida não haverá necessidade de modificá-la.

Se você nunca utilizou interfaces de uma olhada em https://www.tutorialsteacher.com/csharp/csharp-interface (em inglês) é um tutorial bem simples explicando como implementá-las em C#.

OCP (Open Closed Principle) ou Princípio Aberto-Fechado

Este princípio diz que uma classe deve ser aberta para extensão e fechada para modificação. Ok, mas o que isso quer dizer na prática? Vejamos alguns trechos de código (novamente em C#).

Suponhamos que você esteja criando um sistema relacionado a inteligência de mercado que deverá analisar o perfil dos clientes de acordo com diferentes categorias de questionário:

public enum TipoPesquisa {
  HabitosDeConsumoCliente = 1,
  SatisfacaoDoCliente = 2,
  AnalisePublicoAlvo = 3
}

public class GeradorQuestionario {

 public Questionario Gerar(TipoPesquisa tipo) {
  if (tipo == TipoPesquisa.HabitosDeConsumoCliente) {
   // Perguntas relacionadas a hábitos de consumo
  }
  if (tipo == TipoPesquisa.SatisfacaoDoCliente) {
   // Perguntas relacionadas a satisfação do cliente
  }
 }
}

Como podemos enumerar as opções de pergunta, basta ir acrescentando if’s e o problema estará resolvido, certo? A resposta é, para um problema simples como esse, dois ou três if’s a mais não vão fazer muita diferença. Mas imagine uma situação real e mais complexa onde seu sistema estaria evoluindo por meio de if’s! Isso com certeza não seria nada bom. Como uma possível solução poderíamos utilizar a seguinte abordagem:

public abstract class GeradorQuestionario {
 public abstract Questionario Gerar();
}

public class PesquisaHabitosDeConsumoCliente: GeradorQuestionario {
 public override Questionario Gerar() {
  // Gera o questionário com perguntas relacionadas a pesquisa atual
 }
}

public class PesquisaSatisfacaoDoCliente: GeradorQuestionario {
 public override Questionario Gerar() {
  // Gera o questionário com perguntas relacionadas a pesquisa atual
 }
}

public class PesquisaAnalisePublicoAlvo: GeradorQuestionario {
 public override Questionario Gerar() {
  // Gera o questionário com perguntas relacionadas a pesquisa atual
 }
}

Voila! Criamos uma classe abstrata e a herdamos em classes separadas onde podemos ter diferentes implementações do método Gerar. Ficou fácil agora estender o comportamento dessas classes sem a necessidade de realizar modificações a todo momento.

LSP (The Liskov Substitution Principle) – Princípio da Substituição de Liskov

Este princípio, formulado pela professora Bárbara Liskov junto com a vice-presidente da Microsoft Research Jeannette Wing, diz que “Uma classe base deve poder ser substituída pela sua classe derivada”, isto é, classes derivadas estendem de classes bases, mas sem alterar seu comportamento.

Aqui vai um exemplo tosco didático para que fique mais claro o conceito

 public abstract class Veiculo {
  public virtual void Acelerar() {}
  public virtual void Frear() {}
  public virtual void TransporteDeContainer() {}
 }

 public class Moto: Veiculo {
  public override void TransporteDeContainer() {
   throw new NotImplementedException();
  }
 }

 public class Caminhao: Veiculo {
  public override void TransporteDeContainer() {}
 }

Apesar de uma Moto e um Caminhão serem veículos capazes de acelerar e frear é improvável que uma moto seja capaz de transportar um container, ou seja, temos uma classe derivada que viola o princípio de Liskov, pois a classe base não pode ser substituída pela classe derivada, teríamos um método sem implementação. Neste caso poderíamos separar em duas classes, VeiculoTransporteCarga e VeiculoTransportePassageiro por exemplo.

ISP (Interface Segregation Principle) – Princípio da Segregação da Interface

Este é simples, como próprio nome diz se trata de segregar, separar interfaces em novas interfaces mais específicas, mas por que você faria isso? Imagine que você tenha uma interface como a abaixo, para um sistema de fast food:

interface AcoesUsuarioSistema {
 void AcompanharPedido();
 void GerarPedido();
 void GerenciarEstabelecimento();
 void DefinirCardapio();
}

class Administrador: AcoesUsuarioSistema {
 public void AcompanharPedido() {}
 public void DefinirCardapio() {}
 public void GerarPedido() {}
 public void GerenciarEstabelecimento() {}
}

class Funcionario: AcoesUsuarioSistema {
 public void AcompanharPedido() {}
 public void DefinirCardapio() {}
 public void GerarPedido() {}
 public void GerenciarEstabelecimento() {}
}

Repare que tanto o administrador do estabelecimento como um funcionário qualquer não deveriam realizar as mesmas ações, isto é, teríamos métodos sem implementação nos dois casos, o que podemos fazer é separar em interfaces mais específicas:

 interface AcoesUsuarioAdministrador {
   void GerenciarEstabelecimento();
   void DefinirCardapio();
  }

  interface AcoesUsuarioFuncionario {
   void AcompanharPedido();
   void GerarPedido();
  }

  class Administrador: AcoesUsuarioAdministrador {
   public void DefinirCardapio() {}
   public void GerenciarEstabelecimento() {}
  }

  class Funcionario: AcoesUsuarioFuncionario {
   public void AcompanharPedido() {}
   public void GerarPedido() {}
  }

Agora sim, todos realizam suas respectivas funções.

DIP (Dependency Inversion Principle) – Princípio da Inversão de Dependência

O último, mas não menos importante, é o DIP. Este princípio nos diz que o ideal é não depender de implementações (uma classe dependendo de outras classes), mas sim de abstrações (interfaces). Depender de outras classes dificulta o reúso pois ao utilizá-la em outros lugares precisamos levar junto todas as suas dependências. É o velho jargão “programe para interface e não para implementação”.

Vamos ao nosso exemplo para entender melhor, suponhamos que você tenha uma classe responsável por gerar relatórios em PDF:

public class Relatorio {
 private Vendas _vendas;
 private Leads _leads;
 private Produtos _produtos;

 public void ExportarRelatorioVendas() {
  _vendas.gerarPDF();
 }
 public void ExportarRelatorioLeads() {
  _leads.gerarPDF();
 }

 public void ExportarRelatorioProdutos() {
  _produtos.gerarPDF();
 }
}

É possível notar as várias dependências que a classe Relatorio possui, ou seja, ela depende das classes Vendas, Leads e Produtos, violando o princípio DIP. A solução neste caso é inverter a dependência e ao invés de Relatorio depender de Vendas por exemplo, Vendas e Relatorio dependeriam de uma abstração. O mesmo vale para as demais classes, veja:

public interface IRelatorio {
 void GerarPDF();
}

public class Vendas: IRelatorio {
 public void GerarPDF() {}
}
public class Leads: IRelatorio {
 public void GerarPDF() {}
}
public class Produtos: IRelatorio {
 public void GerarPDF() {}
}

public class Relatorio {
 public void Exportar(IRelatorio relatorio) {
  relatorio.GerarPDF();
 }
}

Agora poderíamos passar como parâmetro para o método Exportar qualquer instância que implemente IRelatorio, assim:

var relatorio = new Relatorio();
var vendas = new Vendas();
var leads = new Leads();
relatorio.Exportar(vendas);
relatorio.Exportar(leads);

Conclusão

A conclusão que tiramos aqui é que para alguns casos não existe milagre, é provável que de início você não saiba quais interfaces criar ou quais métodos utilizar numa classe base garantindo sua estabilidade e uso posterior, até porque isso exige, a princípio, planejamento e conhecimento prévio das regras de negócio. Mas separar as responsabilidades, encontrar pontos que flexibilizem a modelagem e manter um código limpo vai com certeza evitar dores de cabeça no futuro.

Bom, por hoje é só pessoal, até o próximo artigo!

Compartilhe!