This chapter explains often used directories under app
and their policies. In the latter half of the chapter, recommended implementation policies for services
directory will be introduced.
Note that it is not necessary to incorporate all. Please consider incorporate some of them according to your situation.
What is the Service class?
It manages business logic are collectively called Service class(es).
In Rails, they are often used to solve Fat model
, but some of them organize View or Controller. When you find an unfamiliar directory, you may understand its role easier if you consider what it belongs to (Model/View/Controller).
Service classes and the directories under app
are necessary to choose that suits the system and team. Please do not copy Web article saying and consider them thoroughly.
Note that, if CI is not already used, I recommend that you first introduce CI, rubocop, and rspec(CI is Github Actions, Circle CI, etc.). Introducing rubocop and rspec is more effective than creating new directory for Service class.
Directories
models
Created by default. Also created "concerns" directory under models
by default.
About concerns
directory is explained in the Model chapter, but be careful how to use it.
controllers
Created by default. Also created "concerns" directory under the "controllers" directory by default.
About concerns
directory is explained in the Controller chapter, but be careful how to use it as well.
views
Created by default. See the View chapter.
helpers
Created by default. see the View chapter as well.
jobs
Created by default. Manages ActiveJob class.
workers
Same as jobs
, since ActiveJob was introduced in Rails 4.2, this name may be used on systems that existed before 4.2, or on systems that do not use ActiveJob.
mailers
Created by default. Manages ActionMailer class.
channels
Created by default. Manages ActionCable class.
If you don't need this directory, you can delete it after removing dependent code.
assets
Created by default. Manages files for the Asset Pipeline.
In the past, a javascripts directory was included by default, but now not created.
javascript
Created by default. Manages Webpacker files.
services
Manages business logic.
It is good to use with policies. On the other hand, if there are no suitable policies, members will create their own style Service class and it might be hell. The recommended implementation policies of this class will be introduced below.
The directory name may be another name depending on your project policy.
forms
Manages business logic that mainly format and validate user input. So-called Form objects. It is often includes Activemodel
and has attributes.
Same as services
, if there are no implementation policies, members will create their own style class and it might be hell.
In my opinion, I don't want to recommend to use this, because if it include Activemodel
, it tends to has larger responsibilities and ambiguous. The responsibilities will be included in services
.
Since this is maybe personal preference, you can use forms
if implementation policies are defined.
decorators
Manages logic for View. See the chapter "View, Helper, and Decorator".
presenters
Manages logic for View.
I have never used this directory, according to some Web articles, Presenter seems to be used for multiple Models while Decorator for a single Model. It is not based on Gems or Rails features, so implementation policies should be defined.
serializers
A relatively major directory that manages the Serializer class to generate json for the APIs.
Basically, Serializer classes takes a Model instance and, extracts and formats items, and returns json. By using the serializer, the items are written as a allowlist(whitelist), so eliminates the worry of unintentionally sending secure items when adding columns or refactoring.
There are some gems the jbuilder
, active_model_serializers
and jsonapi-serializer
, ect. but I can't find best (and well maintained) one. It may be a good to start without gems and then seek ways to solve problems when they arise.
When not use gems
You can also use the as_json
and only
option, which is a standard function. In particular, I recommend using these for Hash.
Since as_json
can be difficult if items increases or the hierarchy deeper, define as a Hash and use as_json
for it instead of using as_json
for Model or ActiveRecord_Relations. So, it will maintain some readability.
In addition, you can use app/serializers
directory even without gems. Separating the serializing methods from the Controller improves readability and easier debugging.
uploaders
Automatically created by carrierwave
, a major gem for file uploading.
Manages uploading settings such as file format and destination directory, ect.
exceptions / errors
Manages custom error classes.
I have not found much benefit in defining custom error classes, so I do not feel it is necessary to create this directory, but if you decide that it would be better to have this, you may do so.
validators
A relatively major directory that manages custom validator classes.
Custom validation had explained in the Model chapter, so see that. Mitigates the Fat Model by separating Validations from app/models
.
deliveries
Manages notification logics.
If the notification process in your system is so large, this directory seems to be a good. I have never used this, so this is just an introduction. Since I did not find any gems, you may need to decide your project policies.
policies
Manages permissions.
The gem Pundit
is mainly used to separate the authorization logics from Controller and View. It is recommended to consider introducing when you have many authorization patterns, or when you often have to display/hide a part of a page according to authorization. If there are only a few authorization patterns, it is enough to manage them using with application_controller.rb
.
queries
Manages SQL-related classes.
The directory purpose is to resolve the Fat model and the Service classes for Fat model more finer.
But, I don't recommend introducing this because it often leads to excessive DRY (excessive refactoring). Basically, just the following approaches are fine.
- Simple conditions may be used in various features are written in Model scope.
- Complex conditions used for a specific feature are written in
app/services
.
lib(/lib, /app/lib)
Place logics that can be used by other applications such as manipulating zip, pdf or calling external APIs, etc.
Depending on the project, it may be placed under root, while others may be placed under app. Since there are no clear rules about lib
, your team should set a policy on this so as not to avoid ambiguity in responsibilities of this directory.
Recommended implementation policies for services
If each coders make Service classes in their own way, it will be very difficult to maintain. To maintain readable code, like the following implementation policies should be set.
- Implement as a Plain Class.
- It does not include or inherit anything such as
active model
, so-called PORO(Plain-Old Ruby Object). - If multiple values need to be returned, return them as a Hash, not a structure or class.
- It is acceptable to define and instantiate attributes for use inside the class
- It does not include or inherit anything such as
- The class name should be easy to understand what it does.
UserFinder
(resource +er
form of verb) orFindUser
(verb + resource) mainly either of these.- More detailed names are better. e.g.
ActiveUserFinder
. - Avoid ambiguous class names. e.g.
UserManager
.
- Only class method named
call
can be used.
Implementation examples
Base
class Users::ActiveUserFinder
def self.call(user_id)
User.find(user_id)
end
end
# Usage
user = Users::ActiveUserFinder.call(user_id)
This is the most basic. I wrote this for the explanation, but you don't need to create a class for this simplicity level.
Multiple arguments
Use keyword arguments for clarity.
class Users::ActiveUserFinder
def self.call(arg1: arg1, arg2: arg2)
(abbreviated)
end
end
# Usage
user = Users::ActiveUserFinder.call((arg1: arg1, arg2: arg2))
Multiple return values
If there are multiple return values, use Hash. I think it is simplest to use Hash, not Structure or Class.
class Users::ActiveUserFinder
def self.call
(Abbreviated)
{erorrs: erorrs, user: user}
end
end
# Usage Example
results = Users::ActiveUserFinder.call
if results[:errors]
# failure handling
else
# success handling
end
Larger class (instantiate internally)
class Users::ActiveUserFinder
def self.call(user_id)
new(user_id).call
end
def initialize(user_id)
@user_id = user_id
end
def call
foo
bar
end
private
attr_reader :user_id
def foo
# Process using user
end
def bar
# Process using user
end
end
# Usage
user = Users::ActiveUserFinder.call(user_id)
In this case, user_id is defined as attr_reader to make it easier to use repeatedly. In terms of simplicity, it is not as simple as using only class methods case, but if you have more arguments or private methods, you will feel very convenient this. attr_reader
is for internal use only, so it is not necessary to know what attributes are there from the outside.
If you want to use user
many times, you can define the following method.
def user
@user ||= User.find user_id
end
It may be included private_class_method :new
to prevent instantiation from the outside, but you may not to need it If the policy is shared.
Summary
These are the policies that I have found to work well in some projects (of course, there are some differences in each project). The idea core is that one class must have only one use (public method) and must not be used for multiple purposes.
Since it is not used for various features like Model, private methods can increase to some extent as long as the code organized. It is not necessary to be as sensitive as Model.
If it becomes larger or has common functions, it is acceptable to create child classes with the same policies, but the class increase may makes difficulty to read, so a sense of balance is important.
Use the policies in other classes
Since there are no strict rules for Job, Rake Task and seed.rb, using this policies will make the whole code more consistent and improve readability.
Directories Examples
This is an article introducing the directories under APP.
This is a famous Rails OSS GitLab
.
Putting README.md for each extended directory, as GitLab does, seems like a pretty good idea to imitate when there are many directories.