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