Law of Demeter simples em Ruby com a gem demeter

Depois 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 :)

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

Esta entrada foi publicada em ruby e marcada com a tag , , , , , , . Adicione o link permanenteaos seus favoritos.
  • @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
  • Para activerecord existe uma gem com funcionalidade parecida: http://github.com/midas/has_associative_facades
  • Caraca, é um comportamento meio bizarro, vou tentar explicar lá.
    Abraço!
  • 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
  • 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!
  • @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.
  • @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.
  • Joao
    @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.
  • 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
  • @andre
    Você tem total razão. Eu só não gosto muito dessa sintaxe. Acho ela pouco clara.

    Obrigado pelo comentário.
  • André
    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!
  • Realmente todos esquecem desse princípio básico do Law of Demeter. Mas muito legal essa gem! Parabéns!
blog comments powered by Disqus