An opinionated implementation of the Command pattern for Ruby applications. Cuprum wraps your business logic in a consistent, object-oriented interface and features status and error management, composability and control flow management.
Commands are the core feature of Cuprum. In a nutshell, each Cuprum::Command
is a functional object that encapsulates a business logic operation. A Command provides a consistent interface and tracking of result value and status. This minimizes boilerplate and allows for interchangeability between different implementations or strategies for managing your data and processes.
Each Command implements a #call
method that wraps your defined business logic and returns an instance of Cuprum::Result
. The result has a #status
(either :success
or :failure
), and may have a #value
and/or an #error
object.
The recommended way to define commands is to create a subclass of Cuprum::Command
and override the #process
method. By convention, #process
is a private method and should not be called directly.
class BuildBookCommand < Cuprum::Command
private
def process(attributes)
Book.new(attributes)
end
end
command = BuildPostCommand.new
result = command.call(title: 'The Hobbit')
result.class #=> Cuprum::Result
result.success? #=> true
book = result.value
book.class #=> Book
book.title #=> 'The Hobbit'
There are several takeaways from this example. First, we are defining a custom command class that inherits from Cuprum::Command
. We are defining the #process
method, which takes a single attributes
parameter and returns an instance of Book
. Then, we are creating an instance of the command, and invoking the #call
method with an attributes hash. These attributes are passed to our #process
implementation. Invoking #call
returns a result, and the #value
of the result is our new Book.
Because a command is just a Ruby object, we can also pass values to the constructor.
class SaveBookCommand < Cuprum::Command
def initialize(repository)
@repository = repository
end
def process(book)
if @repository.persist(book)
success(book)
else
error = Cuprum::Error.new(message: 'unable to save book')
failure(error)
end
end
end
books = [
Book.new(title: 'The Fellowship of the Ring'),
Book.new(title: 'The Two Towers'),
Book.new(title: 'The Return of the King')
]
command = SaveBookCommand.new(books_repository)
books.each { |book| command.call(book) }
Here, we are defining a command that might fail - maybe the database is unavailable, or there’s a constraint that is violated by the inserted attributes. If the call to #persist
succeeds, we’re returning a Result with a status of :success
and the value set to the persisted book.
Conversely, if the call to #persist
fails, we’re returning a Result with a status of :failure
and a custom error message. Since the #process
method returns a Result, it is returned directly by #call
.
Note also that we are reusing the same command three times, rather than creating a new save command for each book. Each book is persisted to the books_repository
. This is also an example of how using commands can simplify code - notice that nothing about the SaveBookCommand
is specific to the Book
model. Thus, we could refactor this into a generic SaveModelCommand
.
A command can also be defined by passing a block to Cuprum::Command.new
.
increment_command = Cuprum::Command.new { |int| int + 1 }
increment_command.call(2).value #=> 3
If the command is wrapping a method on the receiver, the syntax is even simpler:
inspect_command = Cuprum::Command.new { |obj| obj.inspect }
inspect_command = Cuprum::Command.new(&:inspect) # Equivalent to above.
Commands defined using Cuprum::Command.new
are quick to use, but more difficult to read and to reuse. Defining your own command class is recommended if a command definition takes up more than one line, or if the command will be used in more than one place.
Calling the #call
method on a Cuprum::Command
instance will always return an instance of Cuprum::Result
. The result’s #value
property is determined by the object returned by the #process
method (if the command is defined as a class) or the block (if the command is defined by passing a block to Cuprum::Command.new
).
The #value
depends on whether or not the returned object is a result or is compatible with the result interface. Specifically, any object that responds to the method #to_cuprum_result
is considered to be a result.
If the object returned by #process
is not a result, then the #value
of the returned result is set to the object.
command = Cuprum::Command.new { 'Greetings, programs!' }
result = command.call
result.class #=> Cuprum::Result
result.value #=> 'Greetings, programs!'
If the object returned by #process
is a result object, then the result is returned directly.
command = Cuprum::Command.new { Cuprum::Result.new(value: 'Greetings, programs!') }
result = command.call
result.class #=> Cuprum::Result
result.value #=> 'Greetings, programs!'
Each Result has a #status
, either :success
or :failure
. A Result will have a status of :failure
when it was created with an error object. Otherwise, a Result will have a status of :success
. Returning a failing Result from a Command indicates that something went wrong while executing the Command.
class PublishBookCommand < Cuprum::Command
private
def process(book)
if book.cover.nil?
return Cuprum::Result.new(error: 'This book does not have a cover.')
end
book.published = true
book
end
end
In addition, the result object defines #success?
and #failure?
predicates.
book = Book.new(title: 'The Silmarillion', cover: Cover.new)
book.published? #=> false
result = PublishBookCommand.new.call(book)
result.error #=> nil
result.success? #=> true
result.failure? #=> false
result.value #=> book
book.published? #=> true
If the result does have an error, #success?
will return false and #failure?
will return true.
book = Book.new(title: 'The Silmarillion', cover: nil)
book.published? #=> false
result = PublishBookCommand.new.call(book)
result.error #=> 'This book does not have a cover.'
result.success? #=> false
result.failure? #=> true
result.value #=> book
book.published? #=> false
Back to Documentation | Versions | 1.1