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 PDF
s e ODF
s. 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 if
s 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.
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
0sem comentários ainda
Por favor digite as duas palavras abaixo