百科.dev
全部条目在线工具AI 编程趋势榜开源项目技术资讯提交条目
登录
< 返回工具列表
W

waterfall

> 编程语言
开源

一个功能性编程片段用于链接 Ruby 服务和代码块,从而提供一种新的流程控制方法。让它们流动吧!

618 stars0 点赞2 次浏览
访问官网GitHub

工具介绍

一个功能性编程片段用于链接 Ruby 服务和代码块,从而提供一种新的流程控制方法。让它们流动吧!

Goal

Chain ruby commands, and treat them like a flow, which provides a new approach to application control flow.

When logic is complicated, waterfalls show their true power and let you write intention revealing code. Above all they excel at chaining services.

Material

Upcoming book about failure management patterns, leveraging the gem: The Unhappy path

General presentation blog post there: Chain services objects like a boss.

Reach me @apneadiving

Overview

A waterfall object has its own flow of commands, you can chain your commands and if something wrong happens, you dam the flow which bypasses the rest of the commands.

Here is a basic representation:

  • green, the flow goes on, chain by chain
  • red its bypassed and only on_dam blocks are executed.

Example

ruby
class FetchUser
  include Waterfall

  def initialize(user_id)
    @user_id = user_id
  end

  def call
    chain { @response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}") }
    when_falsy { @response.success? }
      .dam { "Error status #{@response.code}" }
    chain(:user) { @response.body }
  end
end

and call / chain:

ruby
Flow.new
    .chain(user1: :user) { FetchUser.new(1) }
    .chain(user2: :user) { FetchUser.new(2) }
    .chain  {|outflow| puts(outflow.user1, outflow.user2)  } # report success
    .on_dam {|error, context| puts(error, context) }         # report error

Which works like:

Installation

For installation, in your gemfile:

gem 'waterfall'

then bundle as usual.

Waterfall mixin

Outputs

Each waterfall has its own outflow and error_pool.

outflow is an Openstruct so you can get/set its property like a hash or like a standard object.

Wiki

Wiki contains many details, please check appropriate pages:

  • Predicates
  • Wf Object
  • Testing

Koans (!)

You can try and exercise your understanding of Waterfall using the Koans here

Illustration of chaining

Doing

ruby
Flow.new
    .chain(foo: :bar) { Flow.new.chain(:bar){ 1 } }

is the same as doing:

ruby
Flow.new
    .chain do |outflow, parent_waterfall|
      unless parent_waterfall.dammed?
        child = Wf.new.chain(:bar){ 1 }
        if child.dammed?
          parent_waterfall.dam(child.error_pool)
        else
          parent_waterfall.ouflow.foo = child.outflow.bar
        end
      end
    end

Hopefully you better get the chaining power this way.

Syntactic sugar

Given:

ruby
class MyWaterfall
  include Waterfall
  def call
    self.chain { 1 }
  end
end

You may have noticed that I usually write:

ruby
Flow.new
    .chain { MyWaterfall.new }

instead of

ruby
Flow.new
    .chain { MyWaterfall.new.call }

Both are not really the same:

  • the only source of information for the gem is the return value of the block
  • if it returns a Waterfall, it will apply chaining logic. If ever the waterfall was not executed yet, it will trigger call, hence the convention.
  • if you call your waterfall object inside the block, the return value would be whatever your call method returns. So the gem doesnt know there was a waterfall involved and cannot apply chaining logic... unless you ensure self is always returned, which is cumbersome, so it's better to avoid this

Syntax advice

ruby
# this is valid
self
  .chain { Service1.new }
  .chain { Service2.new }

# this is equivalent
self.chain { Service1.new }
self.chain { Service2.new }

# this is equivalent too
chain { Service1.new }
chain { Service2.new }

# this is invalid Ruby due to the extra line
self
  .chain { Service1.new }

  .chain { Service2.new }

Tips

Error pool

For the error_pool, its up to you. But using Rails, I usually include ActiveModel::Validations in my services.

Thus you:

  • have a standard way to deal with errors
  • can deal with multiple errors
  • support I18n out of the box
  • can use your model errors out of the box

Conditional Flow

In a service, there is one and single flow, so if you need conditionals to branch off, you can do:

ruby
self.chain { Service1.new }

if foo?
  self.chain { Service2.new }
else
  self.chain { Service3.new }
end

Halting chain

Sometimes you have a flow and you need a return value. You can use halt_chain, which is executed whether or not the flow is dammed. It returns what the block returns. As a consequence, it cannot be chained anymore, so it must be the last command:

ruby
self.halt_chain do |outflow, error_pool|
  if error_pool
    # what you want to return on error
  else
    # what you want to return from the outflow
  end
end

Rails and transactions

I'm used to wrap every single object involving database interactions within transactions, so it can be rolled back on error. Here is my usual setup:

ruby
module Waterfall
  extend ActiveSupport::Concern

  class Rollback < StandardError; end

  def with_transaction(&block)
    ActiveRecord::Base.transaction(requires_new: true) do
      yield
      on_dam do
        raise Waterfall::Rollback
      end
    end
  rescue Waterfall::Rollback
    self
  end
end

And to use it:

ruby
class AuthenticateUser
  include Waterfall
  include ActiveModel::Validations

  validates :user, presence: true
  attr_reader :user

  def initialize(email, password)
    @email, @password = email, password
  end

  def call
    with_transaction do
      chain { @user = User.authenticate(@email, @password) }
      when_falsy { valid? }
        .dam { errors }
      chain(:user) { user }
    end
  end
end

The huge benefit is that if you call services from services, everything will be rolled back.

Undo

If you get to dam a flow, this would trigger the reverse_flow method in all Services previously executed.

reverse_flow is not executed on the service which just failed, consider the on_dam hook in this case.

Take this as a hook to undo whatever you need to undo if things go wrong. Yet, you probably do not need to bother about databases inserts: this is the purpose of with_transaction.

FYI

Flow is just an alias for the Wf class, so just use the one you prefer :)

Examples / Presentations

  • Check the wiki for other examples.
  • Structure and chain your POROs.
  • Service objects implementations.
  • Handling error in Rails.

Thanks

Huge thanks to robhorrigan for the help during infinite naming brainstorming.

Issues· 0 开放

查看全部 Issues在 GitHub 打开

暂无开放 Issues,或尚未同步最近议题。

> 标签

Ruby

暂无评论,来聊聊你的看法吧

> 工具信息

发布日期2026年8月1日
最后更新2026年9月17日
分类编程语言
定价开源

> 相关工具

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言