O objeto Result que esta aplicação já tem

Prévia grátis 6 min de leitura

Em setembro de 2026, app/services nesta aplicação tinha 65 arquivos Ruby. Quarenta e nove deles declaram um tipo de resultado com uma linha no formato desta, copiada de app/services/auth/authenticate_user_service.rb:

Result = Data.define(:success, :user, :error)

Quarenta e quatro desses quarenta e nove põem :success em primeiro. Ninguém escreveu um framework para isso acontecer. Todo service enfrentou o mesmo problema, porque quem chama precisa saber se o trabalho aconteceu. A mesma resposta continuou parecendo razoável, e a forma se espalhou.

O padrão, inteiro, em código real

Este é o service de autenticação completo, como está no repositório:

module Auth
  class AuthenticateUserService
    Result = Data.define(:success, :user, :error)

    # Burned once at boot so unknown emails cost the same BCrypt work as known ones
    # (prevents timing-based user enumeration).
    DUMMY_PASSWORD_DIGEST = BCrypt::Password.create("timing-equalizer", cost: BCrypt::Engine.cost).to_s.freeze

    def self.call(email:, password:)
      user = ::Identity::User.find_by(email: email.to_s.strip.downcase)

      return Result.new(success: false, user: nil, error: I18n.t("auth.errors.account_locked")) if user&.locked?

      if user
        authenticated = user.authenticate(password)
      else
        BCrypt::Password.new(DUMMY_PASSWORD_DIGEST) == password.to_s
        authenticated = false
      end

      if authenticated
        user.reset_login_attempts!
        Result.new(success: true, user: user, error: nil)
      else
        user&.record_failed_login!
        Result.new(success: false, user: nil, error: I18n.t("auth.errors.invalid_credentials"))
      end
    end
  end
end

O controller que o consome, app/controllers/auth/sessions_controller.rb, decide pelo resultado. Este trecho mostra a decisão e omite o que um login bem-sucedido faz em seguida:

# app/controllers/auth/sessions_controller.rb (trecho)
result = Auth::AuthenticateUserService.call(
  email: params[:email],
  password: params[:password]
)

if result.success
  # ... inicia a sessão e redireciona ...
else
  @error = result.error
  flash.now[:alert] = @error
  render :new, status: :unprocessable_content
end

Esse é um bom service e um bom controller. A fronteira é explícita, a falha é um valor e não uma exceção, e o desvio se lê bem. Três campos, um if. Nada aqui precisa de conserto.

Onde a forma começa a pesar

Agora o mesmo padrão num trabalho mais difícil, Enrollment::CompleteLessonService:

Result = Data.define(:success, :lesson_completion, :xp_awarded, :next_lesson,
                     :course_completed, :level_reached, :awarded_badges, :errors)

Oito campos. Uma falha precisa fornecer os oito, então o arquivo carrega um helper privado cuja única função é inventar valores vazios para os sete que já não significam nada:

def self.failure(next_lesson, errors)
  Result.new(success: false, lesson_completion: nil, xp_awarded: 0, next_lesson: next_lesson,
             course_completed: false, level_reached: nil, awarded_badges: [], errors: errors)
end

xp_awarded: 0 não é um fato sobre uma conclusão de aula que falhou. É enchimento, exigido porque sucesso e falha dividem a mesma forma.

Há um segundo custo nesse arquivo, mais discreto que o enchimento. Dentro da transação, o service chama quatro colaboradores:

lesson_xp = ::Enrollment::AwardXpService.call(
  user: user, source: "lesson_completion", course: lesson.course, idempotency_key: "lesson_completion:#{lesson.id}"
)
::Enrollment::UpdateStreakService.call(user: user)
course_result = ::Enrollment::CompleteCourseService.call(user: user, course: lesson.course)
xp_awarded = lesson_xp.amount + course_result.xp_awarded

unlocks = if xp_awarded.positive?
  ::Enrollment::EvaluateUnlocksService.call(user: user, xp_before: xp_before)
else
  ::Enrollment::EvaluateUnlocksService::NOTHING
end

Dois deles devolvem um Result com um campo success, e este código nunca o lê. Parece um caminho de falha ignorado, então confira antes de acreditar. AwardXpService e UpdateStreakService nunca devolvem success: false. Todo Result.new nos dois arquivos passa success: true. Eles avisam de problema levantando erro: update! no service de sequência, um ArgumentError para chave de idempotência ausente no service de XP. O bloco inteiro roda dentro de ActiveRecord::Base.transaction, então um erro levantado desfaz tudo e sai do método.

Ou seja, nada é engolido. O campo success não carrega informação nesses dois services. Lê-lo seria inútil, e ali a convenção é decoração.

Os outros dois colaboradores têm formas diferentes ainda. O CompleteCourseService declara Result = Data.define(:completed, :xp_awarded), sem campo success. Seu completed: false é uma resposta legítima de "este curso ainda não terminou", não uma falha. O EvaluateUnlocksService devolve level_reached e awarded_badges, e tem um resultado vazio pronto, NOTHING, para conclusões que não pagaram XP.

Esse é o estado do padrão nesta aplicação. Um objeto Result com booleano, aplicado por hábito, às vezes a operações que não podem falhar e às vezes sem o booleano. A aula 7 pergunta se vale substituí-lo.

O que este curso avalia

Dry-rb é uma família de bibliotecas Ruby, e duas delas tratam do problema acima. dry-monads faz de sucesso e falha dois valores diferentes, onde a aplicação tem um valor com um booleano. dry-validation tira a checagem de entrada dos models e a coloca em contratos independentes.

Saiba disto antes de procurar documentação: dry-rb.org hoje responde com um redirect 301 para hanakai.org/dry. As bibliotecas passaram para o guarda-chuva da Hanakai, ao lado de Hanami e ROM, e o site chama a família de "Dry". Os nomes das gems não mudaram, então você ainda instala dry-monads e dry-validation. Posts de blog e respostas mais antigas apontam para um domínio que já não serve a documentação diretamente.

Nenhuma das duas gems está instalada nesta aplicação. Um grep por dry no Gemfile e no Gemfile.lock não devolve nada. Todo exemplo de dry-rb deste curso rodou com dry-monads 1.11.0 e dry-validation 1.11.1 num ambiente descartável, e a saída mostrada é a que apareceu. Nada disso roda nesta aplicação hoje. O código Rails citado ao lado é real e atual.

Essa distância é o ponto. Um padrão vale a adoção quando remove um custo que você já consegue apontar em código que você já tem. Este curso nomeia os custos primeiro e depois mede as bibliotecas contra eles.

O que as sete aulas cobrem

A aula 2 troca o booleano success por dois valores distintos e mostra o que muda para quem chama. A aula 3 cobre fmap e bind, as duas operações que deixam passos se comporem sem um if entre cada par. A aula 4 chega à notação Do, que dispensa o helper de falha de oito campos do CompleteLessonService. A aula 5 passa aos contratos do dry-validation, schemas e rules, incluindo um curto-circuito que surpreende quase todo mundo na primeira vez. A aula 6 conecta um contrato a um service por uma linha de extensão. É fácil esquecê-la, e o erro que aparece sem ela é confuso. A aula 7 faz a pergunta que motiva o curso, com números reais: esta aplicação deveria adotar algo disso?

A resposta muda de um service para outro, e a aula 7 diz onde ela é não.

Aula 1 de 7 · grátis

Continue com a aula 2

Success e Failure como valores

Troca o booleano success pelas classes Success e Failure e mostra como value! falha com barulho num Failure.

Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.

Digite para buscar...