13. DELEGAÇÃO

Experiência é o nome que as pessoas dão aos seus erros.”

Oscar Wilde

Na verdade, ninguém espera que um dado objeto “saiba” realizar todas as operações. Talvez, os objetos se beneficiem do mesmo argumento que nós humanos usamos para cumprir até a mais extravagante solicitação: “eu não sei, mas eu sei quem sabe”.

13.1 Conceito de Delegação

Os objetos podem disponibilizar diversos métodos e não necessariamente acomodar a lógica para cumprí-los. Em vez, um objeto pode disponibilizar a interface para uma operação, mas delegar o comportamento para outro objeto. Portanto, o conceito de delegação é a representação de propriedades e recebimento de operações (o recebedor) que são, na verdade, parte de outro objeto e/ou repassadas a este (o destinatário).

A delegação ajuda a manter uma alta coesão. Isto é, os objetos delegados agem apenas como um representante do objeto responsável. Portanto, a lógica concreta de uma certa funcionalidade é mantida em classes separadas. Por exemplo, considere a classe Livro sem utilizar o recurso de delegação:

public class Livro {
  private final String titulo;
  private final List<String> autores = new ArrayList<>();

  public Livro(String titulo, String... autores) {
    this.titulo = titulo.toUpperCase();
    this.autores.addAll(Arrays.asList(autores));
  }

  public String getTitulo() {
    return this.titulo.toUpperCase();
  }

  public String getAutorPrincipal() {
    return this.autores.get(0);
  }

  public void tornarAutorPrincipal(String nome) {
    int indice = autores.indexOf(nome);
    if (indice < 0) {
      throw new IllegalStateException("Autor não faz parte da lista");
    }

    this.autores.add(0, this.autores.remove(indice - 1));
  }
// ...

Neste exemplo, a classe Livro tem a responsabilidade de gerenciar a ordem dos autores. No entanto, a rigor, a lógica está ligada a outro substantivo, o Autor e, portanto, outra classe de objetos. Vamos introduzir a classe de Autores, se aproveitando da já existente ArrayList:

class Livro {
  private Autores autores = new Autores();

  public Autores getAutores() {
    return this.autores;
  }
}
class Autores extends ArrayList<String> {

  public String getPrincipal() {
    return this.get(0);
  }
}

Com esta classe adicional, a responsabilidade da representação e obtenção do autor principal é movida para a classe Autores. Por outro lado, cria-se um efeito colateral: a camada de indireção e o efeito ask, rather than tell (pedir em vez de dizer), por exemplo:

class Main {
  public static void main(String... args) {
    Livro livro = new Livro("Design Patterns",
      "Erich Gamma", "Richard Helm", "Ralph Johnson", "John Vlissides");
    System.out.println(livro.getAutores().getPrincipal()); // <== não queremos isso: getters encadeados
  }
}

Neste exemplo, o Livro não age, de fato, como um delegado dos autores. Em vez, ele devolve a lista de autores (getAutores()) sobre a qual são realizadas novas operações. Métodos get encadeados revelam a estrutura interna de uma classe e, logo, engessam o modelo. Neste caso, nem mesmo há uma delegação, pois o objeto autores é devolvido pelo livro para então obter o autor principal.

A delegação ocorre quando o objeto disponibiliza a interface da operação (ex.: obter o autor principal), mas não é realmente ela que computa a lógica. Neste sentido, a delegação real aconteceria com o seguinte cenário:

class Livro {
  
  private final Autores autores = new Autores();
  // ...
  public String getAutorPrincipal() {   // expõe o método getAutorPrincipal
    return this.autores.getPrincipal(); // mas delega para a classe Autores
  }

  public void tornarAutorPrincipal(String nome) {
    this.autores.tornarPrincipal(nome); // delega para a classe Autores
  }
}

class Autores extends ArrayList<String> {

  public String getPrincipal() {
    return this.get(0);
  }

  public void tornarPrincipal(String nome) {
    int indice = this.indexOf(nome);
    if (indice < 0) {
      throw new IllegalStateException("Autor não faz parte da lista");
    }
    this.add(0, this.remove(indice - 1));
  }
}

Neste exemplo, acontece a divisão de responsabilidades. Objetos Livro continuam disponibilizando o método tornarAutorPrincipal, mas fica a encargo da classe Autores realizar essa mudança (conter a lógica). Livro é um delegado quando se trata de alterar o autor principal.

13.2 Divisão das Responsabilidades

A divisão de responsabilidades é, talvez, um dos maiores desafios dos projetistas de sistemas, sejam estes orientados a objetos ou não. Uma “responsabilidade” pode ser entendida como uma lógica bem definida (conjunto de if's for's, switch'es, etc) usadas para codificar uma operação específica e bem definida. Vários autores se debruçaram (e perderam o sono) sobre este tema, de onde colocar a responsabilidade. Eu vou trazer Craig Larman, que no seu fabuloso livro Usando UML e Padrões, propõe um acronimo, GRASP, e uma abordagem metódica ao projeto orientado a objetos.

General Responsibility Assignment Software Patterns (GRASP), em português: Padrões de Atribuição de Responsabilidades Gerais, é o conjunto de 9 padrões (ou princípios) para a atribuição de responsabilidades. Cinco principais, uma vez entendidos, ajudam no raciocínio de onde codificar a responsabilidade. São eles:

  • Criador (creator): a resposabilidade de criar o objeto A deve ser de B se B (1) contém ou agrega A, grava A, usa A ostensivamente, ou tem os dados necesários para inicializar A;
  • Especialista na Informação (information expert): o objeto que tem as informações para cumprir uma certa funcionalidade deve ser também o reponsável pela funcionalidade;
  • Baixo acoplamento (low coupling): atribuir responsabilidade de modo que o acoplamento seja ou continue baixo;
  • Controlador (controller): este princípios está ligado ao padrão Modelo-Visão-Controlador (MVC) e, justamente, recomenda o uso de um objeto que aja como representante de um sistema, o controlador, que recebe as requições e delega para o subsistema responsável;
  • Alta coesão (high cohesion): atribuir responsabilidades de modo que a coesão continue alta, isto é, que não sejam atribuídos papéis a objetos que não acomodam necessariamente os dados para cumprí-los.

Destes todos, controlador é o que se beneficia do conceito de delegação. No desenvolvimento web backend, isto é, o código que roda nos servidores, são recebidas diversas requisições, representando operações, tal como “adicionar ao carrinho” ou “calcular frete” em um sistema de comércio eletrônico. Não necessariamente a classe que recebe a requisição, por exemplo, na forma de @Put public Result adicionarProdutoCarrinho(String sku, String sessao), será também a responsável por introduzir um registro no banco da dados. Todos os grandes sistemas (e a maioria dos pequenos) são formados por diversas classes com interfaces para operações que são delegadas para outros objetos.

13.3 Princípio da Responsabilidade Única

13.4 Considerações

13.5 Exercícios