Cheeseburgers, Ruby e magia negra
Vamos usar um pouco de magia negra do Ruby para encontrar uma alternativa à implementação clássica do Design Pattern Decorator apresentado pela GoF.

Imagem original de MarketFare Foods, Inc.
.
Este post é a continuação de dois anteriores:
Se você ainda não os leu, recomendo que o faça para entender o contexto do exemplo onde estamos aplicando o Design Pattern Decorator. O ponto onde paramos no último post foi o meu descontentamento em decorar um objeto Cheeseburger de uma forma não muito intuitiva.
Vamos relembrar nosso diagrama de classes:
.
E o nosso último teste:
describe Cheeseburger do
it "should be an Cheeseburger with pepper sauce, onion rings and corn" do
cheeseburger = Corn.new(OnionRings.new(PepperSauce.new(Cheeseburger.new)))
cheeseburger.description.should == "Bread, Hamburger, Cheese, Pepper Sauce, Onion Rings, Corn"
cheeseburger.calories.should == 530
end
end
O teste espera que seja criado um cheeseburger com molho de pimenta, cebola e milho. O teste passa, mas a forma como o cheeseburger é decorado na linha 3 me desconforta.
Então vamos utilizar o poder e a flexibilidade do Ruby, a.k.a. black magic (ou magia negra), para melhorar essa situação. Mas antes vamos dar uma olhada como funcionam os módulos.
.
Um pouco sobre módulos
Módulos em Ruby são grupos de métodos, constantes e variáveis de classes. Os módulos não podem ser instanciados e não existe herança de módulos.
Vejamos um exemplo:
module MyLogging
def info(text)
puts "INFO: #{text}"
end
def warn(text)
puts "WARNING: #{text}"
end
end
.
Os métodos de instância de um módulo podem ser “misturados” em outras classes, é o chamado mixin. Podemos usar o método include para incluir módulos em classes:
class MyService
include MyLogging
def other_method
# method code here
end
end
service = MyService.new
service.info "Some message"
service.warn "Another message"
O método include recebe qualquer número de objetos Module, permitindo assim a inclusão de vários módulos em uma classe.
Outra maneira de “mixar” módulos em uma classe é através do método extend. A diferença é que dessa forma o módulo é incluído em um objeto instanciado e não na própria classe. Veja um exemplo:
module MyLogging
def info(text)
puts "INFO: #{text}"
end
def warn(text)
puts "WARNING: #{text}"
end
end
module MyMailer
def send(to, subject, message)
# method code here
end
end
class MyService
def other_method
# method code here
end
end
service = MyService.new
service.extend MyLogging
service.extend MyMailer
service.send "email@domain.com", "Subject", "The body of the e-mail"
service.info "Message sended."
O método extend insere um módulo na árvore de herança dos objetos, antes da sua classe regular.
Obs.: Módulos podem ser usados também como namespaces, mas não entrarei em detalhes sobre isso aqui.
.
Decorando com módulos
Agora vamos decorar nossos cheeseburgers de uma forma mais “Ruby Way”.
A primeira coisa a ser feita é transformar todas nossas classes decoradoras (Corn, PepperSauce e OnionRings) em módulos:
module Corn
def description
"#{super}, Corn"
end
def calories
super + 70
end
end
module OnionRings
def description
"#{super}, Onion Rings"
end
def calories
super + 140
end
end
module OnionRings
def description
"#{super}, Onion Rings"
end
def calories
super + 140
end
end
O método super irá chamar o método de mesmo nome da classe onde o módulo for incluído.
Podemos testar nossos módulos mixando-os em alguma classe que possua os métodos description e calories:
class FakeSandwich
def description
"Sandwich description"
end
def calories
0
end
end
describe Corn do
before(:all) do
@fake_sandwich = FakeSandwich.new
@fake_sandwich.extend Corn
end
it "should add corn description to sandwich description" do
@fake_sandwich.description.should == "Sandwich description, Corn"
end
it "should add corn calories to sandwich calories" do
@fake_sandwich.calories.should == 70
end
end
Nesse teste criamos a classe FakeSandwich e incluímos o módulo Corn nela (linha 14). Depois testamos os métodos description e calories, assegurando assim que suas implementações no módulo Corn estão corretas.
A nossa classe Cheeseburger não irá mudar (pelo menos por agora):
class Cheeseburger
attr_reader :description, :calories
def initialize
@description = "Bread, Hamburger, Cheese"
@calories = 300
end
end
.
Agora vamos ver como fica aquele teste do início do post:
describe Cheeseburger do
it "should be an Cheeseburger with pepper sauce, onion rings and corn" do
cheeseburger = Cheeseburger.new
cheeseburger.extend Corn, OnionRings, PepperSauce
cheeseburger.description.should == "Bread, Hamburger, Cheese, Pepper Sauce, Onion Rings, Corn"
cheeseburger.calories.should == 530
end
end
Quando o método calories da variável cheeseburger é chamado na linha 6, estamos chamando o método calories do último decorador, ou seja, do módulo PepperSauce. Veja o que acontece:
- PepperSauce chama o método super, que irá chamar o método calories do módulo OnionRings;
- OnionRings chama o método super, que irá chamar o método calories do módulo Corn;
- Corn chama o método super, que irá chamar o método calories da classe Cheeseburger;
- Cheeseburger retorna 300 calorias;
- Corn adiciona suas 70 calorias ao retorno de Cheeseburger e retorna 370 calorias;
- OnionRings adiciona suas 140 calorias ao retorno de Corn e retorna 510 calorias;
- PepperSauce adicona suas 20 calorias ao retorno de OnionRings e retorna 530 calorias.
Para ficar mais intuitivo acrescenter ingredientes nos cheeseburgers, podemos criar um álias para o método extend com o nome de with:
class Cheeseburger
attr_reader :description, :calories
def initialize
@description = "Bread, Hamburger, Cheese"
@calories = 300
end
alias_method :with, :extend
end
.
E o teste modificado:
describe Cheeseburger do
it "should be an Cheeseburger with pepper sauce, onion rings and corn" do
cheeseburger = Cheeseburger.new
cheeseburger.with Corn, OnionRings, PepperSauce
cheeseburger.description.should == "Bread, Hamburger, Cheese, Pepper Sauce, Onion Rings, Corn"
cheeseburger.calories.should == 530
end
end
Hum… já está ficando melhor. Mas ainda temos uma questão: o último módulo adicionado é o primeiro a ser chamado. Sendo assim, continuamos decorando a classe Cheeseburger na ordem inversa que o método description irá retornar.
Para resolver isso de uma vez por todas, vamos remover o alias method e criar um novo método chamado with. Esse método irá receber um array de módulos e chamar o método extend passando esses módulos. Mas antes de passar o array de módulos para o método extend, vamor reverter sua ordem utilizando o método reverse.
class Cheeseburger
attr_reader :description, :calories
def initialize
@description = "Bread, Hamburger, Cheese"
@calories = 300
end
def with(*ingredients)
self.extend *ingredients.reverse
end
end
.
E olha como fica nosso teste agora:
describe Cheeseburger do
it "should be an Cheeseburger with pepper sauce, onion rings and corn" do
cheeseburger = Cheeseburger.new
cheeseburger.with PepperSauce, OnionRings, Corn
cheeseburger.description.should == "Bread, Hamburger, Cheese, Pepper Sauce, Onion Rings, Corn"
cheeseburger.calories.should == 530
end
end
Muito bom! Um novo cheeseburger com molho de pimenta, cebola e milho, exatamente na ordem como está descrito no nosso teste.
Se você quiser criar um cheeseburger com ingredientes utilizando somente uma linha, poderá fazer assim:
cheeseburger = Cheeseburger.new.with PepperSauce, OnionRings, Corn
.
Veja como fica o diagrama de classes com essas alterações:
Mas e quanto à classe Sandwich? Não precisamos mais dela, podemos apagá-la sem problemas. E como o Fabio Kung diz: “Apagar código é melhor do que escrever código bom”.
Em relação à implementação em C# feita no primeiro post, tivemos a redução de 6 artefatos (classes e interfaces) para 4 artefatos (classe e módulos). Isso totalizou a eliminação de 100 linhas de código.
.
Dúvidas, questionamentos, discórdias, sugestões? Deixe seu comentário.
O código completo você encontra disponível aqui no meu Github.
.
Referências:
- Design Patterns in Ruby - Russ Olsen - Addison-Wesley
- The Ruby Programming Language - David Flanagan, Yukihiro Matsumoto - O’Reilly
- Jay Fields’ Thoughts: Ruby extend and include




Comentários