Fork me on GitHub

Keep Learning Conhecimento nunca é o bastante

Postado em
4 May 2011 @ 2:34

Tag(s)
Desenvolvimento, Rails, Ruby

SOLID Ruby: Single Responsibility Principle

Utilizamos BDD e técnicas do programação orientada a objetos não apenas para obter código mais limpo e bonito. Na verdade, essas são consequências do principal objetivo: criar código que tenha baixo custo de manutenção, isto é, não demande muito tempo e pessoas para correções e melhorias.

Um conjunto de técnicas que podemos utilizar para atingir esse objetivo é chamada de SOLID, um acrônimo que representa cinco técnicas:

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

O objetivo desses princípios é fazer com que alterações necessárias sejam feitas no menor número possível de locais no código. Em outras palavras, é diminuir o custo dessas mudanças através de um design que reduz os efeitos colaterais das modificações.

Nesse artigo vou mostrar um pouco sobre a aplicação do Single Responsibility Principle em Ruby.

Esse princípio nos diz que uma classe deve ter apenas uma responsabilidade e deve executá-la por completo (não devem haver outras classes que executem partes dela). É uma forma de conseguir alta coesão, uma qualidade desejável em código orientado a objetos. Uma classe coesa executa completamente uma responsabilidade, ou seja, essa responsabilidade não fica fragmentada e espalhada entre diferentes entidades no domínio.

Uma forma de pensar sobre o que é uma responsabilidade para facilitar a absorção do conceito é que esta representa uma razão para mudar. Então podemos definir o princípio como: uma classe tem uma e apenas uma razão para mudar.

Vamos a um exemplo de um modelo ActiveRecord que viola esse princípio (alerta para pseudo-código que nem deve funcionar, é apenas um exemplo):

class Game < ActiveRecord::Base
  belongs_to :category
  validates_presence_of :title, :category_id, :description,
                        :price, :platform, :year
 
  def get_official_price
    open("http://thegamedatabase.com/api/game/#{name}/price?api_key=ek2o1je")
  end
 
  def print
    <<-EOF
      #{name}, #{platform}, #{year}

      current value is #{get_official_price}
    EOF
  end
end

Esse modelo tem como sua responsabilidade principal cuidar da lógica de negócio ligada à entidade Game. Porém, como podemos ver aqui, ele também está responsável por consultar um webservice para obter a cotação do jogo e por formatar sua exibição. Essa implementação possui baixa coesão pois essa classe possui mais de um motivo para mudar. Ela será alterada se os requisitos de validação de dados mudarem, se algum detalhe da chamada do webservice mudar ou se precisarmos exibir seus dados numa formatação diferente.

Uma forma de resolver isso seria desmembrar essa classe tendo esse resultado:

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

Assim aumentamos bastante a coesão de nosso sofware. Cada classe possui apenas uma razão para mudar e atende ao SRP.

Um exemplo mais real pode ser visto no modelo Creditcard do Spree. Note que, entre outras coisas, o modelo também é responsável pelos processos de compra e autorização do cartão (métodos purchase e authorize). Note como esse métodos implementam parte do processo e delegam outras partes para outros componentes quando a responsabilidade deveria ser 100% realizada em outra parte. Isso cria baixa coesão (múltiplas responsabilidades por classe) e alto acoplamento (uma responsabilidade dividida por várias classes interdependentes).

Como um exercício a parte podemos pensar sobre até que ponto modelos do ActiveRecord quebram o SRP por serem responsáveis por persistência e lógica de negócios (que inclui validação de dados). Há muitos desenvolvedores dos dois lados nessa questão. Acredito que há pequenas violações mas de uma forma não prejudicial, já que a persistência é implementada por uma classe especializada e apenas herdada em nossos modelos, de forma que essa é delegada ao framework (que cuida de possíveis modificações necessárias à persistência).

É preciso ter em mente que ser muito extremista com esses princípios também pode levar à problemas e a um excesso de preciosismo que torna-se prejudicial.

Bom, a partir daqui vamos para outros artigos onde serão explorados os outros princípios. O pseudo-código utilizado no exemplo acima ainda tem algumas coisas a ganhar através da aplicação de outros princípios.

Leitura recomendada:


4 Comentários

Comentário por
Um pouco de SOLID Ruby | Blog Gonow
4 May 2011 @ 19:09

[…] O objetivo desses princípios é fazer com que alterações necessárias sejam feitas no menor número possível de locais no código. Em outras palavras, é diminuir o custo dessas mudanças através de um design que reduz os efeitos colaterais das modificações. Cada um desses conceitos merece uma investigação mais detalhada. Conheça então um pouco mais sobre a aplicação do Single Responsibility Principle em Ruby acessando o post de Lucas em seu blog pessoal. […]


Comentário por
Fabio Alves
4 May 2011 @ 20:47

Lucas, ótimo artigo.

Eu só achei estranho aquela chamada à classe GamePriceService diretamente da classe GamePrinter. Mas isso é assunto para outro post.

Uma lista com links interessantes: gist.github.com/524462
Por acaso, foi você mesmo que fez essa lista. : )

Abraço.


Comentário por
Lucas Húngaro
4 May 2011 @ 22:09

Pô Fabio, assim não vale, isso é spoiler! 😉

Valeu pelo comentário!


Comentário por
Rodrigo Reginato
9 October 2014 @ 10:07

ótimo exemplo


Deixe um comentário