Abstração de mais ou de menos?
Leia em 1 minuto
Em um projeto que comecei a trabalhar recentemente tínhamos uma action que cuja responsabilidade era renderizar uma lista de produtos. Algo como este trecho de código:
class ProductsController < ApplicationController
def index
@products = Product.with_state(:published).limit(34).load
respond_with @products
end
end
Embora seja apenas uma única linha de consulta, fiz um refactoring alterando essa única linha para isso:
@products = Vitrine.load(34)
Essa abstração introduziu uma nova classe chamada Vitrine
, que quebrei em diversos métodos menores.
class Vitrine
def self.load(size, finder = Product)
new(size, finder).load
end
def initialize(size, finder)
@finder = finder
@size = size
end
def published
@finder.with_state(:published)
end
def limit
@finder.limit(@size)
end
def scope
published.merge(limit)
end
def load
scope.load
end
end
OMG! Trocar uma linha com 56 caracteres por um novo arquivo com 26 linhas e 347 caracteres? Faz sentido?
Ao meu ver, faz todo o sentido. Por ter quebrado essa consulta em uma nova classe, tive a flexibilidade de poder escrever testes completamente desacoplados do Rails.
require "test_helper_lite"
require "./app/models/vitrine"
class VitrineTest < ActiveSupport::TestCase
test "delegates to the initializer" do
finder = stub
instance = stub
instance.expects(:load)
Vitrine
.expects(:new)
.with(10, finder)
.returns(instance)
Vitrine.load(10, finder)
end
test "scope searches by state" do
finder = stub
vitrine = Vitrine.new(10, finder)
finder
.expects(:with_state)
.with(:published)
vitrine.published
end
test "limits result" do
finder = stub
vitrine = Vitrine.new(10, finder)
finder
.expects(:limit)
.with(10)
vitrine.limit
end
test "merges scope" do
published = stub
limit = stub
finder = stub
vitrine = Vitrine.new(10, finder)
vitrine.stubs(limit: limit, published: published)
published.expects(:merge).with(limit)
vitrine.scope
end
test "loads products" do
finder = stub
scope = stub
vitrine = Vitrine.new(10, finder)
vitrine.stubs scope: scope
scope.expects(:load)
vitrine.load
end
end
Perceba que estou apenas validando a interface desta classe, o que exige que eu escreva testes de integração, ou estarei testando apenas stubs.
O ponto é o seguinte: até onde esse tipo de abstração traz benefícios? Fui questionado sobre a vantagem de ter trocado uma única linha por vinte e cinco delas.
Minha alteração foi movida pela segurança que escrever este tipo de testes me traz. É infinitamente mais simples encapsular este comportamento e ter testes isolados, do que deixar em um controller e ter testes por lá.
Justificar a implementação de uma determinada funcionalidade não deve ser motivada apenas pelo teste, mas eu quase sempre não consigo fazer a desassociação. Se é difícil de testar, deve existir uma maneira melhor.
Eu continuo acreditando que ter uma classe desacoplada com testes isolados que validam sua interface, juntamente com testes de integração é a melhor abordagem, mas confesso que bateu uma dúvida.
O que você acha? Esse tipo de abstração faz sentido ou é apenas overengineering? O que você faria de diferente?