Skip to content

Latest commit

 

History

History
143 lines (105 loc) · 3.37 KB

README.md

File metadata and controls

143 lines (105 loc) · 3.37 KB

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