Fantoches

From Fragmental Bliki

Conteúdo

Introdução

Eu gosto de Análise e Projeto Estruturados. Primeiro porque me fizeram pensar em programas como mais que bits e bytes correndo por uma tela monocromática, me fizeram entender sistemas.

Depois porque são uma bela técnica para resolver muitos tipos de problemas, principalmente os altamente matemáticos.

Mas o mundo real, ou pelo menos o mundo das aplicações mais comuns, não é feito por funções e sub-rotinas, mas por entidades colaborativas. A modelagem baseada em objetos é vendida como o meio ideal de se desenvolver um sistema nos dias de hoje.

A grande maioria das pessoas acredita nisso também, mas por muitas razões, entre elas o fato de aprenderem a desenvolver software de forma estruturada, acabam utilizando tecnologias orientadas a objetos (linguagens, notações, biblitoecas, ferramentas...) para produzir software de forma estruturada.

A idéia neste artigo é tentar passar um pouco de como não produzir software de maneira estruturada, mas sim Orientada a Objetos.

Mas Estruturar Não Era Legal?

Antes de continuar, deixa-me fazer uma definição que vai servir para o resto deste texto (e do wiki de maneira geral).

O Paradigma Procedural é como eu chamo a programação onde dados e funções estão 
separadas. Nesse   modelo de desenvolvimento de software as entidades são representadas por 
estruturas de dados  simples que são manipuladas por funções independentes.

Eu não gosto do termo Programação Estruturada (apesar de usá-lo ocasionalmente). Este termo surgiu para separar a arte de destruir a seqüência lógica de um sistema com instruções GOTO, comum em BASIC, da separação do fluxo em sub-rotinas, as funções e procedimentos. Dijkstra diz sobre isso tudo no seu paper clássico.

Então, programação orientada a objetos é também estruturada no sentido que separa as funcionalidades em módulos. Chamaremos neste texto de Programação Procedural o modo de definir um programa como descrito acima.

Modelando o Mundo

A primeira coisa que se aprende num curso básico de Análise de Sistemas (seja qual for o paradigma e metodologia usados) é que um sistema de informação modela um mini-mundo. Este mundo é uma versão abstraída do mundo normal, onde os únicos detalhes que importam são os relacionados com a aplicação, com o que o sistema faz.

Num sistema Procedural, geralmente se pensa em como as coisas acontecem, no fluxo de informação. Pensando Proceduralmente, um simples cadastro é assim:

Cliente fornece endereço. Endereço é armazenado.

Como é perceptível, são apenas dados fluindo. Acho que todos imaginam que a função, chamado de Processo na nomenclatura de Análise Estruturada (aquelas bolhas do Yourdon) vai receber uma estrutura de dados ENDEREÇO e colocar no repositório (que pode ser um SGBD ou um arquivo ou qualquer outra coisa).

Pensando de uma maneira OO, você tem:

Cliente tem um endereço.

Uhm? Sim. No seu mini-mundo OO, você vai mapear o que te importa (o cliente e seu endereço) para um objeto. É claro que dependendo da sua aplicação o Cliente pode ser inútil, mas aí vai te restar o objeto Endereço.

Num modelo OO, você está preocupado com o que existe no seu mundo real, como as coisas se relacionam nele.

Implementações Procedurais

Vamos pensar um cenário simples. Um estacionamento.

Nosso sisteminha exemplo tem uma só funcionalidade: descobrir se existe vaga disponível para um carro no estacionamento e colocar esse carro na vaga.

Vamos ver o modo Procedural de fazer as coisas. Vou usar Java para os dois casos, como muita gente programa 100% procedural nesta linguagem, não devemos ter muitos problemas no entendimento.

class Carro {	
 private String placa;	
 private Date dataEntrada;	
 private int vaga = -1;	

 public void setPlaca(String p) {
  this.placa = p;
 }

 public String getPlaca() {
  return placa;
 }
    
 public void setDataEntrada(Date data) {
  this.dataEntrada = data;
 }
 
 public Date getDataEntrada() {
  return dataEntrada;
 }
 
 public void setVaga(int vaga) {
  this.vaga = vaga;
 }
 
 public int getVaga() {
  return vaga;	
 }
}

class Estacionamento {
 public final double VALOR_HORA = 0.5;	
 private Carro[] vagas = new Carro[5];	
 
 public boolean estacionar(Carro c) {
  int vagaLivre = vagaLivre();
  if (vagaLivre == -1)
   return false;

  c.setDataEntrada(new Date()); //coloca a data atual do sistema
 
  vagas[vagaLivre] = c;		
  c.setVaga(vagaLivre);		
  return true;	
 }

 public double sair(Carro c) {
  int vagaOcupada = c.getVaga();
  Date dataEntrada = c.getDataEntrada();
  double conta = tempoEmHorasAteHoje(dataEntrada) * VALOR_HORA;		
  c.setVaga(-1);		
  vagas[vagaOcupada] = null;		
  c.setDataEntrada(null);		
  return conta;	
 }
}

Aposto que muita gente vai estranhar eu rotular isso de procedural. Aqui nós temos a classe Estacionamento que cuida do processo de estacionar e deixar o estacionamento do Carro, representado por um pseudo-JavaBean (um dia eu explico que é um Pseudo-JavaBean na minha concepção).

Mas, o que tem isso de procedural? São classes, não são?

Meu primeiro exercício é para quem tem background em linguagens procedurais. Tente reescrever isso utilizando pascal clássico (não Delphi), FORTRAN ou C (não C++). O código não vai ficar muito diferente, não é? Para quem não programa nessas linguagens, vai mostrar uma implementaçãozinha mixuruca em pseudocodigo baseado em C:

struct t_carro
{
 char * placa ; /* uma string */
 int vaga ;time_t data_entrada ; /* um dos tipos de data */
};

typedef struct t_carro carro ; /*para podermos usar a struct */

carro vazio;
carro vagas[5];

int estacionar(carro c)
{
 int vaga_livre = achar_vaga_livre();

 if(vaga_livre==-1) return 0; /* em C, 0 = false e 1 = true */

 c.data_entrada = data_do_sistema();
 vagas[vaga_livre] = c;
 c.vaga = vaga_livre;
 return 1;
}

double sair(carro c)
{
 int vaga_ocupada = c.vaga;
 time_t data_entrada = c.data_entrada;
 double conta = tempo_em_horas_ate_hoje(data_entrada) * VALOR_HORA;
 c.vaga=-1;
 vagas[vaga_ocupada]=vazio;
 c.data_entrada=0;
 return conta;
}

Mesmo que você não saiba nada de C, dá pra perceber que os códigos são muito parecidos. A maior diferença é a mais fútil, está nas convenções de nomenclatura. Se você colocar os atributos de Carro na primeira implementação como públicos, além de não perder nada (afinal, o que são get e set além de um jeito mais lento de fazer um atributo público?) o código vai ficar mais parecido.

Claro que a sintaxe das linguagens é muito parecida, e isso ajuda, mas o que parece não é a sintaxe, é o meio como se programa. Esse é um programa altamente Procedural.

Note que não existe um só meio Procedural de pensar e implementar ou um só meio OO de fazer isso, uns são melhores outros piores, mas estes são apenas dois exemplos.

Bad Smell: Forças Ocultas

O clássico livro Refactoring, de Martin Fowler introduz o conceito de bad smells, literalmente um fedor no código. Você olha, cheira, vê que tem algo errado, e tem que descobrir a causa.

Note que o sistema funciona! Você pode entregar para o seu cliente exatamente desta maneira, mas o que nos queremos aqui não é entregar algo rápido para gerar uma fatura, estamos estudando a melhor maneira de implementar, e como objetos podem ajudar a tornar o desenvolvimento de software mais natural e mais barato em médio prazo.

Falando em natural, pense no código que criamos, em qualquer uma das implementações. Esse modelo se parece com o mundo real? Vejamos: temos um procedimento que recebe um carro, o joga no estacionamento... como se na entrada do estacionamento houvesse um guindaste que carregasse os carros até suas vagas. É assim que funciona? Não! O motorista ou o manobrista vai pegar o carro e dirigi-lo ate a vaga certa. O carro "sabe" estacionar, e essa é uma responsabilidade dele: dada uma vaga, estacione.

Claro que você pode efetivamente ter seu carro sendo movido desta forma (é só um sistema, não é o mundo real!), mas eu insisto: modele seu sistema o mais próximo que conseguir do mundo real. Isso te ajuda a conversar mais facilmente com seus usuários e manter a mesma linguagem que ele, podendo aproveitar os conceitos que ele aplica no dia a dia.

No caso específico, nosso método estacionar(Carro c) está com responsabilidades demais! Ele tem que coordenar o processo de encontrar uma vaga, modificar o carro, modificar a vaga... e olhe que isso é um exemplo simples, que não envolve acesso a recursos como bancos de dados. Vamos tirar o peso dos ombros do pobre método e colocar nosso carro para ser mais que uma struct de luxo...

class Carro {	
 private String placa;	
 private Vaga vaga;
 
 public void estacionar(Vaga v) {
  this.vaga = v;
  v.ocupar(this);
 }

 public void sair() {
  vaga.setOcupante(null);
  this.vaga = null;
 }

 public Vaga getVaga() {
  return vaga;
 }
}

class Vaga {
 private Date dataEntrada;
 private Carro ocupante;
 
 public void ocupar(Carro c) {
  this.ocupante = c;  
  this.dataEntrada = new Date();
 }

 public double getValor() {
  return (tempoEmHorasAteHoje(dataEntrada) * Estacionamento.VALOR_HORA);
 }

 public long tempoEmHorasAteHoje(Date desde) {
  return 1;
 }

 public void setOcupante(Carro c) {
  this.ocupante = c;
 }
}

class Estacionamento {	
  public static final double VALOR_HORA = 0.5;
  private Vaga[] vagas = new Vaga[5];

  public boolean estacionar(Carro c) {
   Vaga vagaLivre = vagaLivre();
   if (vagaLivre == null)
    return false;

   c.estacionar(vagaLivre);

   return true;
  }

  public double sair(Carro c) {

   Vaga vagaOcupada = c.getVaga();
   c.sair();
   return vagaOcupada.getValor();	
 }
}

Perceba que nesse modelo nós explicitamos um conceito. O cliente no caso quer saber o preço do uso da vaga, não do carro, e para melhor modelar isso nós criamos um objetinho com este conceito.

Observe os programas. Perceba como as responsabilidades foram divididas entre os objetos participantes, como não existe mais um método (ou um conjunto de métodos) que manipula os dados, geralmente cada objeto cuida de si próprio. Essa é a idéia.

De uma maneira geral:

É um bad smell se seus objetos são guiados através de forças ocultas, se seus objetos se 
comportam como marionetes sendo manipuladas por uma função-mestre, o mestre dos fantoches (qualquer    
semelhança com um famoso álbum é mera referência).

Isso é sintoma de um design altamente procedural, onde a lógica de negócio está implementada em funções e em como estas manipulam estruturas de dados. Você geralmente acaba com uma serie de classes (geralmente implementando o padrão Command induzido pelos Frameworks MVC web, mas por vezes são Façades) coordenadoras e um bando de objetos burros que nada mais são que um agrupamento de dados relacionados.

Neste cenário e comum que simplesmente não se consiga reaproveitar lógica. É o tipo de sistema onde reusabilidade e mantida com copiar-e-colar, já que seus métodos são "gordos" demais e são tão especializados que simplesmente não servem para nada alem do que foram concebidos inicialmente. Outro sintoma deste tipo de design são métodos com muitas linhas de código.

Minha sugestão:

Divida a responsabilidade do seu método faz-tudo por objetos que colaborem. Modele melhor seu   
sistema tentando aplicar conceitos e abstrações do mundo real.

Anatomia de um Sistema OO

Um sistema baseado em objetos e formado por componentes (os próprios objetos) que colaboram entre si. Isso não e tão diferente de um sistema procedural, onde as funções e procedimentos devem trabalhar juntos para um baixo acoplamento e alta coesão.

Uma maneira pratica de avaliar seu modelo OO contra fantoches e mestres e o diagrama de seqüência. Observe o diagrama de seqüência de nossa primeira implementação:

O diagrama de interação e exatamente para mostrar como os objetos interagem no sistema. Veja que a interação neste caso e sempre iniciada pelo nosso mestre dos fantoches. Vamos examinar o diagrama de nossa implementação com responsabilidades distribuídas:

Perceberam a diferença? Os objetos agora colaboram entre si, não existe uma única unidade de controle. Se você precisar estender a funcionalidade do seu sistema, agora pode reutilizar os procedimentos que foram separados do método inicial, o método estacionar() agora e apenas um estimulador dos outros objetos.

Entre Flexibilidade e Produtividade

Como sempre, o programador deve lutar entre ser flexível ou ser produtivo. Na verdade, essa e uma luta desnecessária, quando você produz software de qualidade você acaba sendo produtivo em longo e médio prazos.

A grande dificuldade e como saber quando sua arquitetura esta flexível e quando esta flexível demais (mais que o necessário). Você pode perder dias esculpindo um framework em cima da sua aplicação, onde qualquer mudança será muito simples, mas você precisa pensar que pode ser que uma mudança brusca nunca seja necessária, e não há na maioria das vezes como saber as necessidades futuras.

Um bom modelo de objetos vai permitir que você consiga separar bem as coisas. Objetos mapeando significados do domínio, como o exemplo da Vaga, te dão flexibilidade próxima da que seu cliente tem, então se ele resolver mudar seus conceitos você pode seguir a mesma linha de raciocínio ao mudar os seus, incluindo estratégias parecidas de migração no mundo real e no sistema.

Refatorar seu código constantemente ajudar a manter algo simples e flexível. Siga esta pratica.

Conclusão

Todo mundo aprende a construir algoritmos, e algoritmos são dados fluindo entre funções. A migração de paradigma e complexa, e a literatura especializada em plataformas altamente utilizadas, especialmente Java e (principalmente) em Java EE não ajudam nem um pouco, pregando sistemas procedurais.

Existiu um motivo para a criação do paradigma de Orientação a Objetos. Este paradigma provê abstrações melhores e mais naturais, mais próximas do mundo. Antes de definir seu sistema, pense em como as coisas interagem no mundo, pense que objetos não são dados+funções, eles são entidades por si só, e devem colaborar entre si. Não crie "donos da razão" no seu sistema, produza classes coesas e colaborativas.

Se você realmente quer utilizar objetos, fuja o quanto puder dos programas Procedurais. Muitas vezes lidamos com coisas como Bancos de Dados Relacionais e outros subsistemas que são intimamente ligados ao modo Procedural de pensar, porém nestes casos você pode criar um mapeador (algo como o padrão DAO em Java EE) entre os mundos.

Ferramentas pessoais