-
Notifications
You must be signed in to change notification settings - Fork 0
Operations
An Operation is a service object which is executed with a State.
Operations should not be named with the Operation suffix; name them what they do!
class CreateCharge < ApplicationOperation
state_reader :order
state_reader :user
state_reader :payment_method
state_accessor :charge
def behavior
return if charge.present?
state.charge = Charge.create!(payment_method: payment_method, order: order, user: user)
end
private
def payment_method
payment_method.present? payment_method : user.default_payment_method
end
endclass SubmitCharge < ApplicationOperation
failure :charge_unsuccessful
state_reader :charge
state_writer :response
def behavior
charge_unsuccessful_failure!(response_body: response.body) unless success?
charge.update!(success: true)
state.response = response
end
private
def success?
response.body.success == "true"
end
def response
PaymentProcessorClient.submit_charge(charge)
end
memoize :response
endOperations take a state as input and define a #behavior that occurs when #execute is called.
💁 Pro Tip: Operations are just objects! They can be used outside of Flows. Just give them a State (or a State-like object) and you can use them in isolation!
class ExampleOperation < ApplicationOperation
state_reader :first_name
def behavior
puts "Hello, #{first_name}"
end
end
operation = ExampleOperation.new(OpenStruct.new(first_name: "Eric"))
operation.execute
# Hello, Eric
operation.executed? # => trueOperations have access to an wide array of helpful methods.
Operations can access state data with the state method, which returns the State of the flow.
class ChargeFlow < ApplicationFlow
operations CreateCharge,
SubmitCharge
end
class CreateCharge < ApplicationOperation
def execute
charge = Charge.create!(user: state.user, amount: state.order.total)
state.output.charge = charge
end
endOperations can access:
-
state.input- Input data passed to the flow -
state.output- Output data from previous operations -
state- Direct state access (use sparingly)
- Prefer explicit input/output over direct state access
- Keep operations focused on a single responsibility
- Use descriptive operation names that express intent
- Handle errors gracefully within operations
Operations define, through State Accessors, what data they will read from and/or write to a State.
These accessors provide an explicit means to declare input needs and output expectations.
class ExampleOperation < ApplicationOperation
state_reader :foo
state_writer :bar
state_accessor :baz
endUnder the hood, a StateProxy adapts a State to an Operation using these accessors.
The following are considered best practices for working with state accessors:
state_reader should only read data and not alter it
# Bad, don't do it this way:
class BadOperation < ApplicationOperation
state_reader :foo
def behavior
foo << :more_dataz
end
end
# Good, do it this way:
class GoodOperation < ApplicationOperation
state_reader :foo
def behavior
SomeThirdParty.do_a_thing if foo == :foo
end
endstate_writer should map to a field marked as output on the State
state.foo when defining state_writer :foo. Ruby has no way of distinguishing a function scope argument creation from a class scope assignment so it needs to have the prefix
class ExampleOperation < ApplicationOperation
state_writer :foo
def behavior
state.foo = :foo
end
end
# Bad, don't do it this way:
class BadState < ApplicationState
option :foo
end
# Good, do it this way:
class GoodState < ApplicationState
output :foo
endstate_accessor should be used rather than defining both setters and getters:
# Bad, don't do it this way:
class BadOperation < ApplicationOperation
state_reader :foo
state_writer :foo
def behavior
state.foo = :bar if foo == :baz
end
end
# Good, do it this way:
class GoodOperation < ApplicationOperation
state_accessor :foo
def behavior
state.foo = :bar if foo == :baz
end
endstate_accessor should also be used when altering a memory object (ex: Array, Hash, ActiveModel):
# Bad, don't do it this way:
class BadOperation < ApplicationOperation
state_reader :foo
def behavior
foo << :more_dataz
end
end
# Good, do it this way:
class GoodOperation < ApplicationOperation
state_accessor :foo
def behavior
foo << :more_dataz
end
endNext: States