Mostrando postagens com marcador Herança. Mostrar todas as postagens
Mostrando postagens com marcador Herança. Mostrar todas as postagens

novembro 18, 2010

Mais polimorfismo, menos estruturas de controle...

Programadores OO estão acostumados com polimorfismo, porém, programadores da escola procedural ou programadores que estão iniciando no mundo OO, não conhecem ou não enxergam a utilidade prática deste recurso.

A definição do Deitel para polimorfismo é:

"O polimorfismo, permite 'programar no geral' em vez de 'programar no específico'. Em particular, o polimorfismo permite escrever programas que processam objetos que compartilham a mesma superclasse em uma hierarquia de classes como se todas fossem objetos da superclasse."

Mas como isso funciona na prática?

Mesmo com uma linguagem orientada a objetos como o JAVA, é possível escrever programas procedurais. Orientação a objetos está muito mais ligada à decisões de design de software, do que simplesmente à utilização de uma linguagem orientada a objetos.

Por exemplo, usando JAVA, poderíamos escrever o seguinte código:

public class TestAnimais {
 
     enum TipoAnimal{CACHORRO, GATO, GALINHA};
 
     public static void main(String[] args) {
  
          TipoAnimal tipo = TipoAnimal.CACHORRO;
          Animal a = new Animal(tipo);
  
          switch (a.getTipo()) {
               case CACHORRO:
                    System.out.println("ROOF");
                    break;
               case GATO:
                    System.out.println("MEOW");
                    break;
               case GALINHA:
                    System.out.println("CÓ-CÓ-CÓ");
                    break; 
               default:
                    break;
          }
    }
}

Este trecho de código é tipicamente procedural. Programas desenvolvidos utilizando este paradigma, costumam ter muitas instruções de controle do tipo if e switch, para tomada de decisões em tempo de execução.

Podemos refatorar o código acima utilizando uma abordagem polimórfica, orientada a objetos, para reduzir o uso de estruturas de controle (menos 'código espaguete') e tornar o design mais extensível.

Primeiro, criamos uma classe abstrata Animal:

public abstract class Animal { 
     public abstract void emitirSom();
}

Em seguida, criamos uma classe concreta Cachorro, que estende Animal.

public class Cachorro extends Animal {

     public void emitirSom() {
          System.out.println("ROOF");
     }
}

Com este design, nossa 'classe cliente' poderia ficar assim:

public class TestAnimais {
 
     public static void main(String[] args) {
          Animal a = new Cachorro();
          new TestAnimais().digaAlgo(a);
     }
 
     // método ilustrativo
     public void digaAlgo(Animal animal){
          // método polimórfico (Qual animal?)
          animal.emitirSom();
     }
}

O polimorfismo se manifesta no momento em que fazemos uma variável de referência da superclasse (Animal), apontar para um objeto da subclasse (Cachorro) na heap. Desta forma, quando ordenamos que um animal emita som, o método emitirSom() utilizado, é resolvido em tempo de execução baseado no tipo do objeto na heap, sem a necessidade de testes condicionais.

Muitos argumentam que programar orientado a objetos é mais lento. De fato, a curto prazo, esta afirmação muitas vezes se mostra verdadeira.

Os grandes benefícios do paradigma aparecem a médio e longo prazo, principalmente na fase de manutenção do projeto (onde se gasta aproximadamente 80% do tempo).

Por exemplo, se quisermos adicionar um novo animal ao projeto, basta criar um classe concreta (ex.: Gato) que estenda Animal, pois não há risco de comprometer a integridade do que já funciona (design extensível), além do fato de que qualquer método que receba ou retorne um objeto do tipo Animal (ex.: public void digaAlgo(Animal animal)), pode trabalhar normalmente com a nova classe concreta, sem a necessidade de adição de código extra, graças ao polimorfismo.

julho 31, 2010

Grandes poderes trazem grandes responsabilidades...

Herança é um recurso poderoso oferecido pela orientação a objetos, mas para aproveitar suas vantagens, é preciso ter cuidado e fazer uso consciente dela.

Herança é um relacionamento fortíssimo entre duas classes. Quando você diz que um coisa herda outra em orientação a objetos, você está realmente dizendo que: "uma coisa além de ser uma coisa, também é outra coisa". Ahn?! O Que?! Como assim?! Parece até diálogo do Oráculo com o Neo em Matrix. Calma, um exemplo simples pode desfazer toda a confusão. Se eu digo que:

class Cachorro extends Animal{}

De fato, eu estou dizendo que "Cachorro além de ser um Cachorro, também é um Animal". Na prática, qualquer variável de referência do tipo Animal, pode apontar para um objeto de Cachorro na Heap.

Dizer que Cachorro é um Animal, faz todo sentido, nossa classe Cachorro além de possuir uma interface própria, carrega toda a interface referente à Animal. Herança tem um preço, ela gera acoplamento entre classes, elas se tornam intimamente ligadas, mas neste contexto, é perfeitamente aceitável, pois é natural que Cachorro seja um Animal. Agora pense, se eu disser:

"Cachorro é uma Cafeteira!"

...

Esquisto não?!...mas eu insisto que Cachorro é uma Cafeteira e codifico:

class Cachorro extends Cafeteira{}

Este código vai compilar e rodar perfeitamente. Nós acabamos de criar uma aberração, um cachorro que faz café. Criamos um acoplamento completamente desnecessário e reduzimos significativamente a coesão da classe cachorro. Uma classe coesa é altamente especializada e possui responsabilidades bem definidas, não é o caso do nosso "Cachorro-Cafeteira".

Vamos dizer que eu não esteja satisfeito com o fato de que meu Cachorro só saiba fazer café, e diga o seguinte:

"Cachorro é uma Cafeteira, um Microondas, um Celular e um Grill George Foreman."

Se o cachorro que faz café já é esquisto, isso aí então...

Java só aceita Herança Simples, isto é, uma classe só pode herdar de outra classe e ponto final. Mas se mesmo com Herança simples, podemos criar um cachorro que faz café, imagine o que pode ser feito utilizando Herança múltipla. C++ é um exemplo de linguagem que utiliza este recurso, onde uma classe pode herdar de várias classes. Herança simples, mal utilizada, pode resultar em classes com responsabilidades mal definidas, isso gera um ônus alto para um projeto orientado a objetos, logo, permitir que classes herdem de diversas classes, pode ser fatal em termos de qualidade de software, pois nada impede que se crie um "Frankenstein", apenas o bom senso.

Java oferece o recurso de interfaces. Uma interface é "uma classe 100% abstrata". Uma classe pode implementar diversas interfaces. Por mais que nada impeça que sua classe implemente interfaces totalmente sem sentido, existe uma vantagem nesta abordagem, pois Interfaces, são livres de implementação, isto é, seus métodos são "ocos", é de responsabilidade da classe implementadora prover funcionalidades aos métodos.

Herança é uma ferramenta muito útil, oferecida pelo paradigma orientado a objetos, porém, utilizá-la com coerência, é responsabilidade do desenvolvedor que preza pela boa qualidade do projeto de software. Lembre-se: Grandes poderes trazem grandes responsabilidades!