Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Command Class

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:

  1. What your code is doing at a high level is immediately clear (no scanning through code to understand what it does)
  2. All your dependencies are obvious at a glance
  3. Your dependencies can be easily swapped out for test doubles, making it easy to write clean unit tests
  4. 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.

Install

gem install command_class

Example Use

By Extending an Ordinary Class

  1. 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
  1. 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.

  1. Run the command object:
create_user.(name: valid_name, email: valid_email, password: valid_pw)

Using CommandClass.new

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

Motivation

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...

About

Simple Functional Command Objects in Ruby

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages