segunda-feira, 21 de dezembro de 2009

Programação Incondicional III

As linguagens orientadas a objetos trouxeram à programação uma forma muito eficiente de eliminar ifs: o polimorfismo.

A habilidade de tratar homogeneamente objetos de classes diferentes ajuda a eliminar código como este:

if(shape instanceof Circle) {
 drawCircle();
} else if(shape instanceof Square) {
 drawSquare();
}

A solução, evidentemente, é criar uma superclasse e colocar nela um método que as subclasses possam sobrescrever, sem que os clientes dessas classes tenham que se preocupar com qual método estão chamando. No seguinte exemplo, eu crio uma instância de Circle, mas uso uma referência a Shape (sua superclasse). Eu invoco draw() (que deve estar definido em Shape) e o método que vai ser invocado é o que está definido em Circle.

Shape s=new Circle();
s.draw();

Se eu tiver um array de Shape, posso invocar draw() em cada elemento, sem considerar qual método estou realmente invocando.

for(int i=0; i<shapes.length; i++) {
 shapes[i].draw();
}


No entanto, ainda há espaço para eliminar mais ifs! Uma característica da maior parte das linguagens orientadas a objetos é que o método invocado depende do tipo da referência usada, não do tipo do objeto. O seguinte exemplo ilustra bem o que quero dizer. Eu não tento invocar diretamente draw() em cada Shape, mas tento fazê-lo através de outro método draw() da própria classe Shape; eu poderia querer contar os tipos das instâncias ou tomar alguma outra ação.

import static java.lang.System.out;

public class Shape {

 private static int circles, squares;

 public void draw() {
  //
 }

 public static void draw(Shape s) {
  out.println("I drew a shape");
 }

 public static void draw(Circle s) {
  out.println("I drew a circle");
  circles++
 }

 public static void draw(Square s) {
  out.println("I drew a square");
  squares++;
 }

 public static void main(String... args) {
  Shape[] shapes=new Shape[] { new Circle(), new Square() };

  for(int i=0; i<shapes.length; i++) {
   draw(shapes[i]);
  }
 }

}

class Circle extends Shape {

 public void draw() {
  //
 }

}

class Square extends Shape {

 public void draw() {
  //
 }

}


No método main(), eu percorro um array do tipo Shape[] e invoco um método draw() para cada referência. Na classe Shape, há três métodos draw(); cada um recebe um tipo diferente de Shape. No entanto, como eu estou usando referências a Shape no laço, somente o método draw(Shape) é invocado.

Em algumas situações eu posso realmente desejar tomar uma ação diferente, conforme o tipo do objeto, não da referência. Nesse caso, eu estaria de volta a usar ifs e o operador instanceof.

Algumas poucas linguagens usam um tipo de invocação chamado double-dispatch. Nele, o tipo da referência não importa, mas sim o tipo do objeto. Em Java ou C#, não existe esse mecanismo, mas é possível emulá-lo com o padrão Visitor.

Então, vamos reescrever o exemplo acima. Vou criar um PaintVisitor e adicionar um método a Shape.

public class Shape {

 public void accept(PaintVisitor v) {
  visit(this);
 }

}

class Circle extends Shape {

 public void accept(PaintVisitor v) {
  v.visit(this);
 }

}

class Square extends Shape {

 public void accept(PaintVisitor v) {
  v.visit(this);
 }

}

class PaintVisitor {
 public void visit(Shape s) {
  out.println("I drew a shape");
 }

 public void visit(Circle c) {
  out.println("I drew a circle");
 }

 public void visit(Square s) {
  out.println("I drew a square");
 }

 public void draw(Shape s) {
  s.accept(this);
 }

}

Finalmente, eu posso reescrever aquele laço da seguinte maneira:

PaintVisitor visitor=new PaintVisitor();
for(int i=0; i<shapes.length; i++) {
 visitor.draw(shapes[i]);
}

E sem um if sequer!

Em algumas situações, o padrão Visitor pode ser realmente útil. Ao construir um editor de textos, eu descobri que era muito melhor separar o código de desenho dos objetos, porque era difícil dar o contexto necessário para cada objeto desenhar a si próprio. Um parágrafo, por exemplo, pode conter imagens e textos de diferentes tamanhos. Fazer cada elemento calcular sua posição e depois desenhar a si próprio, recursivamente, torna o código deveras complicado e extenso. A solução de criar um Visitor para calcular as posições e outro para desenhar o texto tornou o código muito menor e mais simples.

Quando for preciso percorrer uma estrutura de dados, vale a pena considerar o padrão Visitor.

Nenhum comentário: