Command Class is an implementation of functional command objects, which allow you to cleanly define your business's use cases. A few of the benefits of this style of programming are:
- What your code is doing at a high level is immediately clear (no scanning through code to understand what it does)
- All your dependencies are obvious at a glance
- Your dependencies can be easily swapped out for test doubles, making it easy to write clean unit tests
- Command objects are stateless
Note: Command objects are not identical to the Command design pattern that you may be familiar with from the Gang of Four book.
gem install command_class
- Define your command object class. This is a longish but real-worldish example.
NOTE: dependencies
are static and created once. inputs
are dynamic values passed at runtime.
class CreateUser
extend CommandClass::Include
class InvalidName < RuntimeError; end
class InvalidEmail < RuntimeError; end
class InvalidPassword < RuntimeError; end
class EmailAlreadyExists < RuntimeError; end
command_class(
dependencies: {user_repo: UserRepo, email_service: MyEmailService},
inputs: %i[name email password]
) do
def call
validate_input
ensure_unique_email
insert_user
send_confirmation
end
private
def validate_input
validate_name
validate_email
validate_password
end
def ensure_unique_email
email_exists = @user_repo.find_by_email(@email)
raise EmailAlreadyExists if email_exists
end
def insert_user
@user_repo.insert(name: @name, email: @email, password: @password)
end
def send_confirmation
@email_service.send_signup_confirmation(name: @name, email: @email)
end
def validate_name
valid = @name.size > 1
raise InvalidName unless valid
end
def validate_email
valid = @email =~ /@/
raise InvalidEmail unless valid
end
def validate_password
valid = @password.size > 5
raise InvalidPassword unless valid
end
end
end
- Create your command object itself:
create_user = CreateUser.new
NOTE: Here, alternatively, we can inject dependencies other than the default ones, which vastly improves tests. See the specs for examples of this.
- Run the command object:
create_user.(name: valid_name, email: valid_email, password: valid_pw)
Note: This syntax is provided mainly for backwards compatibility. With this syntax, you cannot define custom errors (or any classes) within the class body.
See the file spec/create_user2.rb
for a full example of this syntax.
Briefly, it looks like this:
CreateUser2 = CommandClass.new(
dependencies: {user_repo: UserRepo, email_service: MyEmailService},
inputs: [:name, :email, :password]
) do
def call
validate_input
ensure_unique_email
insert_user
send_confirmation
end
# rest of class similar to above...
end
On the benefits of Functional Command Objects:
https://www.icelab.com.au/notes/functional-command-objects-in-ruby/
For a more complex, but also more fully-featured version of this idea, see:
https://dry-rb.org/gems/dry-transaction/
More to come...