Fork me on GitHub

Keep Learning Conhecimento nunca é o bastante

Postado em
9 May 2011 @ 23:57

Tag(s)
Desenvolvimento, Ruby, Test-Driven Development

SOLID Ruby: Dependency Inversion Principle

Continuando os artigos sobre SOLID, vamos falar um pouco sobre o Dependency Inversion Principle. Em resumo, esse princípio diz que os componentes devem depender de abstrações ao invés de implementações.

Bom, isso faz muito sentido em linguagens estáticas como Java, onde há estruturas como Interfaces, classes abstratas e outras parafernalhas. No final, na minha modesta opinião, o código torna-se um espetáculo bizarro de indireção e pode mais confundir do que ajudar se você não for cuidadoso. Mas e nas linguagens dinâmicas?

É comum ouvir que, quando você utiliza uma linguagem que contém Duck Typing (a característica que mais gosto no Ruby), você obtém os benefícios do DIP “de graça”. Isso não é bem verdade. Você os obtém de graça somente se pedir com jeitinho. 😉

O que quero dizer com isso?

Vamos ao exemplo do artigo anterior em sua última versão:

class Game < ActiveRecord::Base
  belongs_to :category
  validates_presence_of :title, :category_id, :description,
                        :price, :platform, :year
end
 
class GamePriceService
  attr_accessor :game
 
  # we could use a config file
  BASE_URL = "http://thegamedatabase.com/api/game"
  API_KEY = "ek2o1je"
 
  def initialize(game)
    self.game = game
  end
 
  def get_price
    data = open("#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}")
    JsonParserLib.parse(data)
  end
end
 
class GamePrinter
  attr_accessor :game
 
  def initialize(game)
    self.game = game
  end
 
  def print
    price_service = GamePriceService.new(game)
    <<-EOF
      #{game.name}, #{game.platform}, #{game.year}

      current value is #{price_service.get_price[:value]}
    EOF
  end
end

Esse código possui algumas dependências rígidas. Chamamos as classes GamePriceService e JsonParserLib explicitamente dentro de GamePrinter#print e GamePriceService#get_price, respectivamente. Note que, apesar do Duck Typing nos permitir utilizar componentes com uma determinada interface independente de tipo, fizemos com que nossas classes fiquem amarradas à alguns tipos através dessas dependências.

Uma forma de se aproveitar dos benefícios do Duck Typing e, assim, conseguir “de graça” as vantagens do DIP, é tornar nossas dependências transparentes. Enquanto linguagens estáticas costumam se utilizar (embora isso não seja obrigatório) de complexas bibliotecas de injeção de dependência para fazer isso (ah, meus tempos de Java e Spring :D), em Ruby isso é tão simples quanto passar um parâmetro:

class Game < ActiveRecord::Base
  belongs_to :category
  validates_presence_of :title, :category_id, :description,
                        :price, :platform, :year
end
 
class GamePriceService
  attr_accessor :game, :json_parser
 
  # we could use a config file
  BASE_URL = "http://thegamedatabase.com/api/game"
  API_KEY = "ek2o1je"
 
  def initialize(game, json_parser = JsonParserLib)
    self.game = game
    self.json_parser = json_parser
  end
 
  def get_price
    data = open("#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}")
    json_parser.parse(data)
  end
end
 
class GamePrinter
  attr_accessor :game, :game_webservice
 
  def initialize(game, game_webservice = GamePriceService)
    self.game = game
    self.game_webservice = game_webservice
  end
 
  def print
    price_service = game_webservice.new(game)
    <<-EOF
      #{game.name}, #{game.platform}, #{game.year}

      current value is #{price_service.get_price[:value]}
    EOF
  end
end
 
# Usage example:
game = Game.new(some_game_data)
webservice = GamePriceService.new(game)
game.price = webservice.get_price
 
GamePrinter.new(game).print

Aqui escolhemos passar essas dependências através dos construtores das classes. Dessa forma, ficamos livres da dependência de tipo e passamos a depender apenas de uma interface. Basta que a classe passada no construtor responda aos métodos que utilizamos e não teremos problema. Outra forma seria passar as dependências apenas para os métodos que as utilizam:

class Game < ActiveRecord::Base
  belongs_to :category
  validates_presence_of :title, :category_id, :description,
                        :price, :platform, :year
end
 
class GamePriceService
  attr_accessor :game
 
  # we could use a config file
  BASE_URL = "http://thegamedatabase.com/api/game"
  API_KEY = "ek2o1je"
 
  def initialize(game)
    self.game = game
  end
 
  def get_price(json_parser = JsonParserLib)
    data = open("#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}")
    json_parser.parse(data)
  end
end
 
class GamePrinter
  attr_accessor :game
 
  def initialize(game)
    self.game = game
  end
 
  def print(game_webservice = GamePriceService)
    price_service = game_webservice.new(game)
    <<-EOF
      #{game.name}, #{game.platform}, #{game.year}

      current value is #{price_service.get_price[:value]}
    EOF
  end
end
 
# Usage example:
game = Game.new(some_game_data)
webservice = GamePriceService.new(game)
game.price = webservice.get_price
 
GamePrinter.new(game).print

É uma maneira melhor para evitarmos construtores muito complexos quando esse for o caso. Uma vantagem adicional é obtida através da utilização de valores padrão para os parâmetros. Desta forma, a não ser que queiramos utilizar alguma dependência não-padrão, as dependências ficam totalmente transparentes.

Mais uma vantagem é no momento de escrever testes. Passando dependências dessa maneira, é muito fácil criar objetos dublês e utilizá-los para facilitar o processo (tornar a execução mais rápida, evitar o uso de webservices reais etc). Um exemplos simples e eficiente pode ser visto aqui. (slides 34, 35 e 36)

Em geral, se você não conseguir substituir uma dependência por um dublê em suas especificações, seu código está muito rígido e pode se beneficiar da aplicação desse princípio.

Leitura adicional:

Make Your Dependencies Translucent with Default Parameters


5 Comentários

Comentário por
Guilherme Silveira
10 May 2011 @ 14:42

OI Lucas,

Em Java (e em qualquer outra linguagem OO) fazemos DI através de construtor e métodos, antes do pessoal citar solid em Ruby. Cuidado que a crítica que você está fazendo é contra o Spring, e não uma linguagem específica.

Para um histórico sobre DI – antes da existência do Spring como você o conhece – existe o post do Paul Hammant e do Martin Fowler.

Abraço


Comentário por
Lucas Húngaro
10 May 2011 @ 14:53

Oi Guilherme.

A parte de Java com Spring foi só uma brincadeira. 🙂

O que afirmo é que em linguagens com Duck Typing fazer isso é bem mais fácil (não é preciso usar coisas como sobrecarga de métodos, entre outras técnicas). Não afirmo que em linguagens que não tenham essa característica não dê pra atingir o mesmo objetivo. Modifiquei o artigo pra deixar isso mais claro.

É isso. Um abraço. 🙂


Comentário por
SOLID Ruby: Dependency Inversion Principle | Blog Gonow
23 May 2011 @ 18:26

[…] Acesse a post original com as explicações das telas e também uma leitura adicional para clarear ainda mais esta explanação: http://blog.lucashungaro.com/2011/05/09/solid-ruby-dependency-inversion-principle/ […]


Comentário por
Prodis a.k.a. Fernando Hamasaki de Amorim
14 June 2011 @ 0:21

Supondo que as classe GamePriceService e GamePrinter fiquem nos arquivos game_price_service.rb e game_printer.rb, respectivamente, onde você colocaria esses arquivos? Na própria pasta app/models ou em lib/game?


Comentário por
Lucas Húngaro
14 June 2011 @ 1:23

Oi Fernando!

Bom, eu utilizo uma estrutura bem fora do padrão. Exceto por pequenos hacks, evito usar a pasta lib, que acaba sempre ficando meio relegada à segundo plano.

Dessa forma, classes como essas, que considero tão importantes quando models e controllers, ficam dentro da pasta app.

Veja um exemplo da estrutura de um projeto Rails que estou trabalhando atualmente:

app
   controllers
   extensions (extensões ao framework)
   helpers
   mailers
   models
   services (classes com lógica de negócio que não pertence aos models)
   stylesheets
   uploaders
   views
   wrappers (classes que abstraem acesso a web services)

 
							

Deixe um comentário