Why You Need Service Objects?
The problems
As your application grows you will see domain/business logic scattered across models, controllers and GraphQL resolvers. many logics don’t belong to controllers and resolvers or models, making it difficult to reuse and maintain code.
1
2
3
4
5
class ProductController < ApplicationController
def create
Product.new(*args)
end
end
This seems fine but as your application grows, things can easily become a mess. For example,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProductController < ApplicationController
def create
default_args = { genre: find_category(), location: find_store_location() }
Product.new(attrs.merge(default_args))
end
private
def find_category
// ...
end
def find_store_location
// ...
end
end
Service objects allow you to extract this logic into a separate class. This simplifies the code in a better way.
1
2
3
4
5
6
class ProductController < ApplicationController
def create
ProductCreator.create_product
end
end
Benefits of encapsulating business logic:
- Thin Rails controller and GraphQL resolver: they are mainly responsible for understanding requests and responses.
- Re-usable services
- Testable: Since the logic in a controller or resolver is thinner and simpler, it becomes really easy to test. also, testing business processes becomes more isolated. it is easier to stub and check whether specific processes are invoked within our service.
Implementation
First let’s create a new ProductCreator
And then just dump all our logic inside a new Ruby class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProductCreator
def initialize(name:)
@name = name
end
def create_product
Product.create!(
title: @name
)
rescue ActiveRecord::RecordNotUnique => e
# handle duplicate entry
end
end
end
and then, we can call the service object within the application:
1
2
3
4
5
class ProductController < ApplicationController
def create
ProductCreator.new(name: params[:name]).create_product
end
end
Service Object Syntactic Sugar
We can make the ProductCreator.new(arguments).create_product
chain simpler by adding a class method that instantiates the ProductCreator and calls the create method for us:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProductCreator
def self.call(...)
new(...).call
end
def initialize(name:)
@name = name
end
def call
Product.create!(name: @name)
rescue ActiveRecord::RecordNotUnique => e
# handle duplicate entry
end
end
end
and then, we can call the service object within the application in a simpler way:
1
2
3
4
5
class ProductController < ApplicationController
def create
ProductCreator.call(name: params[:name])
end
end
References
- https://www.toptal.com/ruby-on-rails/rails-service-objects-tutorial
- https://www.honeybadger.io/blog/refactor-ruby-rails-service-object/