Refactoring Your Rails App with Rails Service Objects

As an initial step in the development of a Rails controller application, it is often recommended to begin with the creation of simple models and controllers. However, as the application continues to expand its functionality, the complexity of the models and controllers can quickly become overwhelming. To avoid this, it is necessary to break up these large chunks of code into smaller, more manageable pieces by refactoring them into Rails service objects.

What makes rails service objects so important?

The architecture of the Ruby on Rails service object is conducive to the Model View Controller (MVC) design pattern. If you are looking for something concise and basic, this framework will suffice. However, as your program increases in complexity, you may find that business or domain logic is distributed across both the controller and the models. This type of logic does not belong in either the model or the controller, making the code more difficult to maintain and reuse.

By utilising the Rails Service Object Paradigm, we can separate the business logic from our models and controllers, allowing us to make our models act solely as data layers and the API‘s controller entry point. This allows us to better capture the underlying logic of the system and reap the numerous benefits from doing so. We have developed services such as:

  1. Controls that can be verified

    Since the controllers have been designed to be minimalistic, and they are able to work in conjunction with the service, the process of testing is significantly simplified. As we only require verification that certain methods within the controller are called whenever a particular event occurs, this can be easily achieved.
  2. Reduced-waste rail controllers

    It is the sole responsibility of the controllers to interpret incoming requests and convert session and parameter data into a form that can be used by the service object. The response from the service will determine whether the controller performs a redirect or renders the page. If you take a closer look at more complex applications, you will find that the controller actions which utilise Rails services have more lines of code.
  3. Domain-Framework Isolation

    In the Ruby on Rails framework, controllers can take advantage of services in order to interact with domain objects. By reducing coupling between parts of the system, scalability is improved, especially when transitioning from a traditional monolithic architecture to a microservice architecture. The extraction and migration of existing services to a new environment should be smooth and require no modifications.
  4. Solutions that can be used again

    Rails service objects, commonly known as controllers or queued tasks, are the “brains” of an application. Through the controller, user engagement with models and views can be enhanced, as it helps bridge the gap between incoming requests and the internal entities of the application. Furthermore, the controller is the ideal place to place essential auxiliary services. In essence, the controller helps to streamline the process of converting incoming requests into internal entities.
  5. The ability to conduct solitary tests on business processes

    Since services in Ruby are objects that can be tested separately from their environment, it is much more efficient to test them. This makes it easy to accurately track the amount of time spent by each coworker, allowing us to quickly and easily confirm that the service’s processes were followed.

Service object creation

  1. Starting off, we create a new BookCreator in the services/apps folder of our library management program.
  2. Second, we create a new Ruby class and place all of our logic inside of it.
  3. The third step is to invoke the service object from the controller or elsewhere in the app.

Syntactic sugar for objects that provide services

To reduce complexity in the BookCreator, we may implement a class method that creates a new instance and invokes the create function.

It is possible to create a common ancestor for all service objects, the ApplicationService class, which has an abstract implementation of the call function. This allows us to reuse existing code in order to prevent duplication while creating other similar service instances in Rails.

The BookCreator may be refactored to derive from ApplicationService.

Using the BusinessProcess gem to create Rails service objects

The BusinessProcess gem offers a convenient solution to eliminate the need for specifying an initialisation function or a basic application service class as it comes pre-configured with all the necessary settings. The BusinessProcess::Base class serves as the parent class for any service object.

What you can do in your BusinessProcess gem file

gem ‘business_process’

The best practices for developing Rails service objects are detailed here.

  1. Give each object in Rails’s service directory a descriptive name that reflects the function it serves.

    It is essential that the purpose of a service object be accurately reflected in its name. There is a widely accepted practice of naming Rails service objects with the suffixes ‘er’ or ‘or.’ For example, if the service object’s role is to create a book, then a suitable name may be “BookCreator,” while if its purpose is to read a book, then “BookReader” may be appropriate.
  2. Only one open means

    Due to the importance of keeping business operations and internal methods secure, Service Objects should only expose a single public method that performs the designated business operation. This public method should be clearly documented and named in a consistent manner across all Rails Service Objects. The exact implementation of the public method is up to the developer, as long as the method name remains consistent.
  3. Divide up your Rails service objects into several namespaces.

    Incorporating a service object into a large application provides the potential for scalability; additional service objects can be added as requirements change. To maximise code organisation, it is recommended to arrange frequently used Rails service objects into distinct namespaces. When considering the library application, it is essential to establish a standardised naming convention for all services related to books and authors.

    Calls from us will now officially be known as Book::BookCreator.call(*args) and Book::BookReader.call(*args).
  4. You shouldn’t directly instantiate rails service objects.

    The use of the syntactic sugar pattern and the BusinessProcess gems can be beneficial for streamlining the invocation of Rails service objects. The pattern and gems can be used to simplify expressions such as BookCreator.new(*args).call or BookCreator.new.call(*args), making them more concise and easier to understand. This can be achieved by transforming them into the more comprehensible BookCreator.call(*args).
  5. Get out of trouble and throw exceptions of your own design.

    The objective of the service object is to obscure the details of how Rails ActiveRecord interacts with other services, databases, and frameworks. It is essential that the service is capable of dealing with any ActiveRecord-related issues that may arise in an appropriate manner, such that a chain reaction of errors is avoided. In the event that the error cannot be managed within the rescue block, a custom exception that is specific to the service object should be thrown.
  6. A single duty per service item

    Rails service objects are designed to handle one specific task, which is why it is not recommended to create a generalised service object. If you want to share logic between multiple service objects, you can create a base or helper module and use mixins to incorporate the service into the module. This approach provides a way to reuse logic while maintaining the design philosophy of having each service object perform a single function.

    The implementation of service object pattern will bring a range of new functionalities to your application’s design, making it both easier to maintain and more evocative. Moreover, it will also enhance the testability of the codebase, allowing for the reliable identification and resolution of any issues.

FAQs

  1. Can you tell me what kinds of default environments Rails uses?

    Rails is pre-configured to include three distinct environments: a test, production, and development environment. These environments are consistent and standardised, though some references to them may be found in the underlying Rails source code. This singularity of set up for each application is due to the fact that the configuration for each environment is tailored to the specific application.
  2. Should we call Ruby on Rails a framework?

    The Ruby on Rails framework is a server-side web application framework that is released under the MIT Licence and written in the Ruby programming language. Rails is based on the Model-View-Controller (MVC) architectural pattern, which allows for efficient organisation of the codebase and allows for the separation of concerns for the various components of a web application, such as the website layout, database logic, and web services.
  3. When working with Ruby, what is the difference between a class and a module?

    Modules are often used to group together related constants and procedures. However, modules are not capable of instantiating new objects on their own; classes and similar modules may be composed of other types of modules. It is possible to incorporate constants and methods into the class without interruption, broadening the scope of the class, as there is no parent module to inherit from.

    Furthermore, classes have the capability to generate individual objects, and each object has its own individual characteristics. Classes should not be included in any arbitrary context; while classes can be derived from other classes, modules do not have this ability.
  4. Where do hashes fit in Ruby?

    Ruby hashes are data structures that store distinct key-value pairs. It is possible to use an array as a hash, and any type of object can be used as the key for indexing. However, it is important to note that iterating through the set of returned keys and values does not guarantee that they will be returned in the same order of entry, but rather that they will be returned in some order.

Join the Top 1% of Remote Developers and Designers

Works connects the top 1% of remote developers and designers with the leading brands and startups around the world. We focus on sophisticated, challenging tier-one projects which require highly skilled talent and problem solvers.
seasoned project manager reviewing remote software engineer's progress on software development project, hired from Works blog.join_marketplace.your_wayexperienced remote UI / UX designer working remotely at home while working on UI / UX & product design projects on Works blog.join_marketplace.freelance_jobs