Go to English Blog

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?