Project

operate

0.0
No commit activity in last 3 years
No release in over 3 years
Create service objects with Operate.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 4.2.0
~> 1.14
~> 10.0
~> 3.0
 Project Readme

Operate

Operate is a gem to help create service objects.

Gem Version Build status Code Climate

Use Operate to remove business logic from your controller and model, subsuming it in Operate-based "service" object that represents your processes. Examples might be: a user addition, a post addition, or adding a comment.

Service objects can factor out behavior that would bloat models or controllers, and is a useful step to patterns like Strategy and Command.

Service objects are not a new concept, and extracting controller bloat to service objects is a common refactoring pattern. This Arkency blog post describes extracting service objects using SimpleDelegator, a useful pattern. Operate can assist you with process, further refining it: rather than raising exceptions in your service object, and rescuing exceptions in your controller, we broadcast and subscribe to events.

Operate is in the very earliest stages of development. Additional features will be added. The current API exposed via Operate::Command, however, is solid and no breaking changes there are anticipated.

You can read a little more about Operate in this short blog post.

Dependencies

If ActiveRecord is available, transactions are supported. There is no explicit support for other ORMs.

It's not required, but a form object library like Reform is recommended. Reform is used in the examples below.

Installation

Add this line to your application's Gemfile:

gem 'operate'

And then execute:

$ bundle

Or install it yourself as:

$ gem install operate

Usage

API

Operate's Operate::Command api provides:

Methods used in your service class:

  • #call - perform your business logic
  • #broadcast(:event, *args) will broadcast an event to a subscriber (see on below)
  • #transaction(&block) wraps a block with an ActiveRecord::Base.transaction (only if ActiveRecord is available)

Methods used by clients (normally a controller) of your service class:

  • self#call(*args, &block) will initialize your class with *args and then invoke #call
  • #on(*events, &block) that subscribe to an event or events, and provide a block to handle that event
  • #expose(hash) called within a block passed to #on will set the hash as instance variables on the caller (typically a controller)

A service example

To build a basic operation:

  • Add include Operate::Command to your command class
  • Accept and assign whatever arguments are required in the initialize method
  • Make the call method execute your operation
  • broadcast(:some_event, *results) (results optional) to return the result of your operation
# put in app/services, app/commands, or something like that
class UserAddition
  include Operate::Command
  
  def initialize(form)
    @form = form
  end
  
  def call
    return broadcast(:invalid, @form) unless @form.valid?    
    transaction do
      create_user
      audit_trail
      welcome_user
    end
    
    broadcast(:ok)
  end
  
  def create_user
    # ...
  end
  
  def audit_trail
    # ...
  end
  
  def welcome_user
    # ...
  end
end

And your controller:

class UserController < ApplicationController
  def create
    @form = UserForm.new(params) # a simple Reform form object
    UserAddition.call(@form) do
      on(:ok)      { redirect_to dashboard_path }
      on(:invalid) do |form|
		  expose(form: form)
		  render :new
	  end
    end
  end
end

Note: this example does not use [Strong Parameters] as Reform provides an explicit form property layout.

Passing parameters

You can pass parameters to the handling block by supplying the parameters as arguments to broadcast.

# Your service
class UserAddition
  include Operate::Command
  def call
    # ...
    broadcast(:ok, user)
  end
end


# Your client (a controller):
def create
  UserAddition.call(@form) do
    on(:ok) {|user| logger.info "#{user.name} created" }
  end
end

Exposing values

You can expose a value from within a handler block to the calling controller. Pass the values to expose as a hash.

def new
  UserBuild.call(@form) do
    on(:ok) do |user|
	  expose(user: user)
	  render :new  # new.html.erb can access @user
	end
  end
end

Testing

A straight-forward way to test the events broadcast by an Operate::Command implementor:

class UserAddition
  include Operate::Command
  # ...
  def call
    return broadcast(:invalid) if form.invalid?
    # ...
    broadcast(:ok) 
  end
end
describe UserAddition do
  it 'broadcasts ok when creating user' do
    is_ok = false
    UserAddition.call(attributes_for(:new_user)) do
      on(:ok) { is_ok = true }
    end
    expect(is_ok).to eq true
  end
  it 'broadcasts invalid when user validation fails' do
    is_invalid = false
    UserAddition.call(attributes_for(:invalid_user)) do
      on(:invalid) { is_invalid = true }
    end
    expect(is_invalid).to eq true
  end
end

Credit

The core of Operate is based on rectify and wisper, and would not exist without these fine projects. Both rectify and wisper are excellent gems, they just provide more functionality than I require, and with some philosophical differences in execution (rectify requires you to extend their base class, while operate provides mixins).

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/tomichj/operate. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

Contributors

Many thanks to:

  • k3rni made ActiveRecord dependency optional

License

The gem is available as open source under the terms of the MIT License.