Ir para o conteúdo

Aurium

Voltar a Blog
Tela cheia

SubClasses não! Decorators

15 de Maio de 2013, 21:00 , por Desconhecido - 0sem comentários ainda | No one following this article yet.
Visualizado 284 vezes

Imagine: Você está fazendo um sistema que deve apresentar diversos formatos de dados similares e derivados. O que você faz?

Classes e Subclasses! Yeah!
Não... Pare.

Eu também sempre fui por aí, o projeto Noosfero também começou assim, mas qual é o problema com isso?

Imagine que você quer apresentar arquivos em uma página web. Temos a classe File e sabemos que imagens devem ser apresentadas com a tag <img>, enquanto outros arquivos serão apresentados apenas com o link para download. O que você faz?

Podemos fazer duas subclasses estendendo File: Image e GenericFile — ou — colocamos um "if" na visualização (Ugh... mas é mais econômico).

Ok... Mas agora você decidiu que as imagens serão editáveis e você precisa de um modo de edição diferenciado para bitmaps (PNG, JPG) e vetores (SVG). Depois você decide que vai adicionar um player para áudio (OGG), para vídeos html5 (OGV, WebM) e vídeos flash (FLV, MP4). Vai continuar usando "if"?

Agora não dá para fugir de uma representação com classes especializadas. A primeira idéia seria:

  • File
    • Image
      • ImageBitmap
      • ImageVector
    • Playable
      • Audio
      • Video
        • VideoHTML5
        • VideoFlash
    • GenericFile

No caso de uma aplicação Rails e de tantos outros fameworks (se não todos), a classe que representa o objeto persistido em banco (Model), contendo ou não referência para o sistema de arquivos, é igualmente registrada em banco, frequentemente tendo uma tabela exclusiva para sí. Isso torna a busca por itens do tipo X muito mais fácil e rápido.

Qual o problema com isso? Imagine que agora você decide fazer o preview de documentos de texto, como PDFs e ODFs. Ao atualizar sua aplicação será necessário fazer uma correção no banco onde alguns registros dos tipos em questão serão convertidos para o novo tipo.

"Migrations existem para isso. Não é?" Sim, mas você não quer adicionar complexidade (código extra e espalhado) a sua modificação. Você quer tranquilidade. O admin quer tranquilidade. Atualização simples, admin feliz → Admin feliz, usuário seguro. :-)

Ok, esse argumento ainda não convenceu, então imagine que sua aplicação suporta plugins e alguns deploys irão usar um ou outro plugin para um tipo não suportado no core e, para piorar, o plugin pode ser habilitado e desabilitado a qualquer momento. O que fazer? Você faria com que o método que habilita e desabilita plugins rodasse a migration nesses momentos? (Se você respondeu "Er... Sim." eu tenho medo de você.)

Bem, acho que agora eu convenci que é possível chegar em um ponto onde a criação de subclasses para apresentar tipos derivados pode te levar a calvície prematura ou internação em instituição psiquiátrica. Mas qual é a solução?

Não registre tipos derivados em novas classes! Crie decorators.

Você sabe o que é um decorator? Eu também não estudei padrões de projeto com afinco, mas é bom que você tenha ao menos uma idéia do que isso significa.

No livro Objects on Rails, Avdi Grimm descreve o que ele chama de exhibit, um decorator especializado em apresentar models. Perfeito! Inspirado nele eu trabalhei numa solução para a exibição de arquivos no Noosfero. A idéia é o seguinte:

Pegue aquele diagrama de herança de classes que coloquei no início do texto, não derive File, crie uma árvore independente de exhibits:

  • File (sem subclasses)
  • FilePresenter (o nome que dei ao meu exhibit abstrato)
    • Image
      • ImageBitmap
      • ImageVector
    • Playable
      • Audio
      • Video
        • VideoHTML5
        • VideoFlash
    • GenericFile

Quando você quiser apresentar um arquivo não use render_page_to(some_file), use:

fp = FilePresenter.for(some_file)
render_page_to(fp)

Quando quiser extrair uma informação específica ao tipo do arquivo não use some_file.icon(), isso exigiria uma coleção de ifs neste método, use:

fp = FilePresenter.for(some_file)
fp.icon

A princípio pode parecer que precisaremos de muito código extra coletando o FilePresenter de File antes de qualquer exibição, mas não, veja o meu patch para o Noosfero. Poucos locais precisam usar o método FilePresenter.for() e a tal instância é repassada adiante para outros usos. Essa implementação apenas coloca o (praticamente) mesmo código em locais diferentes, tornando "tudo" mais maleável. Com isso os sérios problemas mostrados antes estão solucionados. Você pode, por exemplo, adicionar e remover um plugin que define um presenter para qualquer tipo específico, a qualquer momento, e tudo funcionará de imediato, sem maiores preocupações (se você não usar POG).

Mas como funcionaria o FilePresenter.for()?

Sim, esta é umas das diferenças da minha implementação para a proposta de Avdi (boa para este caso, mas considere a proposta de Avdi mais abrangente), o método de classe deve perguntar a cada subclasse se ele aceita exibir o arquivo e com qual prioridade.

Por exemplo, se temos uma imagem JPG o FilePresenter.GenericFile deve responder positivamente com um valor baixo, o FilePresenter.ImageBitmap deve responder positivamente com um valor alto e o FilePresenter.Audio deve responder positivamente com um valor nulo. Depois de coletar todas as respostas, o método FilePresenter.for(meu_jpg) criará uma instância da classe que respondeu com maior prioridade, encapsula o objeto File meu_jpg.

Como a subclasse de FilePresenter "responde" se aceita ou não encapsular a instancia de File? Toda subclasse deve implementar o método accepts?(file) como método de classe.

Tudo junto (uma visão simples):

class FilePresenter
  def self.for(f)
    # Intera para cada subclasse, ordenando por prioridade
    klass = FilePresenter.subclasses.sort_by {|class_name|
      # Pergunta se a subclasse aceita o arquivo e sua prioridade
      class_name.constantize.accepts?(f) || 0
    }.last.constantize  # Pega a subclasse de maior prioridade
    # Retorna a instância da subclasse, encapsulando o objeto do arquivo
    klass.new(f)
    # Existem outros detalhes a serem considerados, mas isso basta para o exemplo.
  end

  # Isso fará esse decorator funcionar como uma instância de File
  def method_missing(m, *args)
    @file.send(m, *args)
  end

  ...
end

class FilePresenter::Image < FilePresenter
  def initialize(f)
    @file = f  # encapsula a instância de File
  end

  def self.accepts?(f)
    # retorna alta prioridade para imagens
    f.content_type[0..4]=='image' ? 10 : nil
  end

  ...
end

Fonte: http://softwarelivre.org/aurium/blog/subclasses-nao-decorators

0sem comentários ainda

    Enviar um comentário

    Os campos realçados são obrigatórios.

    Se você é um usuário registrado, pode se identificar e ser reconhecido automaticamente.

    Cancelar