Service is a class that contains some (often overused) logic. Instead of implementing logic in controllers, models, workers, etc., it is implemented in services, and then these services are used in the same controllers, models, etc.
The purpose of this post is to demonstrate how to prepare the base for further development of uniform services in Ruby projects.
Preparation
Before starting to write services, it is necessary to prepare the basis for them in the project. The examples will use gem servactory. This gem will be the basis for all services in the project.
Creation of base class
All services will inherit from the same base class. Create a file app/services/application_service/base.rb
with the following content:
class ApplicationService::Base < Servactory::Base
configuration do
# Learn more: https://servactory.com/getting-started
end
end
All of the project's services will also be located in the app/services
directory.
Creation of service
Most often the following is required of the service:
- should get some data;
- should process the incoming data (if any), and do something additional (if necessary);
- should return some result;
- should be able to work properly with other project services, including services for working with API.
Servactory as a basis implements and controls all of this.
Simple example
As an example, consider a simple service for creating a customer and then notifying them about it.
class CustomersService::Create < ApplicationService::Base
input :email, type: String
input :first_name, type: String
input :middle_name, type: String, required: false
input :last_name, type: String
output :customer, type: Customer
make :create!
make :notify
private
def create!
outputs.customer = Customer.create!(
email: inputs.email,
first_name: inputs.first_name,
middle_name: inputs.middle_name,
last_name: inputs.last_name
)
end
def notify
Notification::Customer::WelcomeJob.perform_later(outputs.customer)
end
end
This service expects 4 arguments, 1 of which is optional. Also this service should return 1 expected attribute. Each of the attributes has a type that the service also expects. If there is a problem at these stages, the service will fall with an error.
The service also has methods, each of which is executed in turn through make
.
There are two ways to call this service - via .call!
or via .call
. Calling via .call!
will always throw an exception. Use the .call
method to check the result of the operation (success or failure) manually.
The controller code will be like this:
def create
service_result = CustomersService::Create.call!(**customer_params)
@customer = service_result.customer
end
# or like this:
# def create
# service_result = CustomersService::Create.call(**customer_params)
#
# if service_result.success?
# redirect_to ...
# else
# flash.now[:message] = service_result.error.message
# render :new
# end
# end
private
def customer_params
params.require(:customer).permit(:email, :first_name, :middle_name, :last_name)
end
Something more complicated
Now let's take a look at the complex example of updating the list of countries in the database. This will demonstrate additional features and interaction between services, including inheritance.
Service for working with API
Requests to the external API are also made through services, and all for the same reason.
Through services written in servactory, control incoming and outgoing attributes by validating their type and controlling their obligation. This allows you to better control the requests and their responses.
In this example, services for working with the API will also be located in the app/services
directory.
The base class
For the final service that works with the API client, a base class is created that will initialize the API client and form the basic rules for working.
class CountriesService::API::Base < ApplicationService::Base
make :perform_api_request!
private
def perform_api_request!
outputs.response = api_request
# The error comes from the client API. For the purposes of this note, we will not delve into it.
# The API client can be implemented and return an error in any way.
rescue CountriesApi::Errors::Failed => e
fail!(message: e.message)
end
def api_request
fail!(message: "Need to specify the API request")
end
def api_client
# CountriesApi is the module of client API.
@api_client ||= CountriesApi::Client.new
end
end
Service for receiving countries
The end service that inherits from the client API base class.
class CountriesService::API::Receive < CountriesService::API::Base
output :response, type: CountriesApi::Responses::List
private
def api_request
api_client.countries.list
end
end
So, to make a request and get a result with a list of countries, just call the service this way:
CountriesService::API::Receive.call
Database update
The result obtained from the external API must be updated in the database. This logic will also be placed in the service. Check this example:
class CountriesService::Refresh < ApplicationService::Base
internal :response, type: CountriesApi::Responses::List
make :perform_request
make :create_or_update!
private
def perform_request
service_result = CountriesService::API::Receive.call
return fail!(message: service_result.error.message) if service_result.failure?
internals.response = service_result.response
end
def create_or_update!
internals.response.items.each do |item|
country = Country.find_or_initialize_by(code: item.code)
country.assign_attributes(**item)
country.save!
end
end
end
The examples above demonstrate the division of code with different logic into classes. Also shown how these classes (services) interact with each other.
As a result, the final service can be called in the controller or worker in the same way:
# Action in the controller
def refresh
CountriesService::Refresh.call!
end
Testing
It is obvious how to test classes and their methods in Ruby. In the case of servactory, it is almost the same, except the approach. Servactory provides a unified view for working with attributes and for result of the service. You should work with this, when writing tests.
Ruby Gem
The examples in this post were based on Servactory.
Repository in GitHub: github.com/servactory/servactory
Documentation: servactory.com
servactory / servactory
Powerful Service Object for Ruby applications
A set of tools for building reliable services of any complexity
Documentation
See servactory.com for documentation.
Quick Start
Installation
gem "servactory"
Define service
class UserService::Authenticate < Servactory::Base
input :email, type: String
input :password, type: String
output :user, type: User
private
def call
if (user = User.authenticate_by(email: inputs.email, password: inputs.password)).present?
outputs.user = user
else
fail!(message: "Authentication failed")
end
end
end
Usage in controller
class SessionsController < ApplicationController
def create
service_result = UserService::Authenticate.call(**session_params)
if service_result.success?
session[:current_user_id] = service_result.user.id
redirect_to service_result.user
else
flash.now[:message] = service_result.error.message
render :new
end
end
private
def session_params
params
…
Top comments (0)