Law of Demeter simples em Ruby com a gem demeter
agile, design, dry, pattern, pragmatic, rails, ruby, tools novembro 26th, 2009Depois de programar algum tempo em Ruby, me senti muito incomodado em ter que repetir um determinado código para manter minha estrutura respeitando a Law of Demeter. Pra quem não está familiarizado, segue um simples exemplo em Rails:
#models class Post < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base belongs_to :post end #view - erb|haml @comment.post.title @comment.post.name @comment.post.something_else
O exemplo é um pouco forçado, mas o problema claro do exemplo é que estamos conhecendo demais sobre o objeto post dentro de comment. Se for necessário alguma alteração em algum dos atributos que estamos acessando diretamente, possivelmente isso resultará em modificações em cascata em todo código.
Depois dessa explicação básica para quem ainda não conhecia a Law of Demeter, vamos aplicar algumas soluções:
Segunda tentativa:
#models
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
def post_title
post ? post.title : nil #preciso verificar se é nulo, caso contrário terei problemas
end
def post_name
post ? post.name : nil #preciso verificar se é nulo, caso contrário terei problemas
end
def post_something_else
post ? post.something_else : nil #preciso verificar se é nulo, caso contrário terei problemas
end
end
#view - erb|haml
@comment.post_title
@comment.post_name
@comment.post_something_else
Essa mudança resolve o problema. Acontece que isso acaba sendo um pattern para resolver o problema, portanto, precisamos encontrar uma forma de não ficar repetindo esse código.
Quem já leu o livro The Pragmatic Programmer, tem bem na memória o capítulo que apresenta o conceito DRY – D’ont Repeat Yourself. Quem programa em Ruby e principalmente já usou o framework Rails sabe bem que DRY é um dos chavões que estão imbutidos na propaganda. Vamos então tentar fazer mais algumas modificações pra tentar alcançar esse objetivo:
Terceira tentativa:
#models class Post < ActiveRecord::Base has_many :comments end require 'forwardable' class Comment < ActiveRecord::Base extend Forwardable belongs_to :post def_delegator :post, :name, :post_name def_delegator :post, :title, :post_title def_delegator :post, :something_else, :post_something_else end #view - erb|haml @comment.post_title @comment.post_name @comment.post_something_else
O módulo Forwardable já vem com o Ruby. Portanto, a solução mais obvia foi usar esse módulo para melhorar o exemplo anterior. Apesar de escrever menos código, essa alternativa tem o inconveniente de não verificar se o objeto post é nil, causando assim NoMethodError em alguns casos. Sendo assim, a alternativa anterior ainda parece ser mais adequada. Porém, a duplicação de código ainda me incomodava bastante, portanto, resolvi montar uma solução única que deu origem a gem demeter.
A solução definitiva:
#no shell > sudo gem update --system > sudo gem sources -a http://gemcutter.org > sudo gem install demeter #models class Post < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base extend Demeter #extends demeter module demeter :post #demeter post object belongs_to :post end #view @comment.post_title @comment.post_name @comment.post_something_else
Basicamente o problema foi resolvido com 2 linhas de código:
extend Demeter demeter :post
A vantagens são visíveis porque (1) você escreve bem menos, (2) já existe a verificação de objetos nulos e (3) caso você queira sobrescrever o comportamento padrão, basta criar um método que responda a mesma mensagem que a gem demeter responde. Dessa forma, o método criado pelo programador semrpe terá prioridade.
O código fonte do projeto está em http://github.com/emerleite/demeter com todas as instruções para utilização tanto em Ruby quanto em Ruby on Rails. O código fonte tem todos os testes automatizados que cobrem diversos cenários. O resultado desses testes podem ser vistos em http://runcoderun.com/emerleite/demeter. A página da gem fica em http://gemcutter.org/gems/demeter
Aguardo o feedback de vocês

novembro 27th, 2009 at 10:15
Realmente todos esquecem desse princípio básico do Law of Demeter. Mas muito legal essa gem! Parabéns!
novembro 27th, 2009 at 10:39
Sensacional! Muito DRY mesmo. Parabéns pela dica.
só uma dica rápida:
Em ruby você não precisa de operadores ternários para validar se a variável é nula, como: post ? post.title : nil
Pode simplesmente fazer: post && post.title
fica um pouco mais simples.
Abraços!
novembro 27th, 2009 at 13:58
@andre
Você tem total razão. Eu só não gosto muito dessa sintaxe. Acho ela pouco clara.
Obrigado pelo comentário.
novembro 28th, 2009 at 9:24
o rails tem um esqueminha que ajuda a combater esse invasão de privacidade das classes, é o método delegate:
http://blog.wyeworks.com/2009/6/4/rails-delegate-method
é bem útil e eu chorei quando vi isso
novembro 28th, 2009 at 15:56
@Emerson Uma sintaxe só é pouco clara quando as pessoas não estão acostumadas.
x ||= 10 não é mais pouco claro pra maioria das pessoas que programam em Ruby, imagino.
novembro 28th, 2009 at 16:24
@roadhouse
Sim, eu sei. Inclusive deveria ter citado no meu post
Porém, ainda acho a solução da minha gem mais DRY, apesar do delegate ser mais flexível.
novembro 28th, 2009 at 16:26
@joao
Questão de opinião e gosto meu caro, rs. Nem tudo que tem na linguagem agente gosta/usa.
De qualquer forma, obrigado pelo seu ponto de vista.
Abraços.
janeiro 12th, 2010 at 11:24
Olá!
Gosto bastante da gem e já estou usando nos projetos.
Mas hoje fui fazer um deploy que roda um rake gems:install no meu servidor de staging, e ele instalou o demeter 2.0.
Na hora de subir o environment ocorreu um erro: uninitialized class variable @@demeter_names in Demeter::ClassMethods
Tive que instalar a versão 1.0.4 (que é a que eu uso em development) para funcionar. Faltou configurar alguma coisa?
Abraços!
janeiro 12th, 2010 at 11:58
Olá andré. Pode ser que tenhamos inserido algum bug no último refactoring. Tem como você abrir um ticket lá que vou dar uma atenção ao teu problema? Se puder colocar as linhas do teu código onde ta acontecendo o erro vai ajudar bastante.
http://github.com/emerleite/demeter/issues
[]s
janeiro 12th, 2010 at 16:04
Caraca, é um comportamento meio bizarro, vou tentar explicar lá.
Abraço!
fevereiro 26th, 2010 at 12:15
Para activerecord existe uma gem com funcionalidade parecida: http://github.com/midas/has_associative_facades
fevereiro 27th, 2010 at 11:52
@Jefferson
Parece sem bem legal a gem. No caso a gem demeter funciona não somente com Rails, mas com qualquer coisa relacionada a Ruby.
[]s