長寿プロジェクトを未来へ – RailsプロジェクトでService Objectsを使う理由

English follows Japanese. 英語の文章は後半にあります。

こんにちは!エンジニアのファットです。今回は、私たちのShareWis(WisdomBase)プロジェクトにおける10年以上続くRuby on Railsアプリケーションの進化についてお話しします。弊社のエンジニアの頑張りのおかげで、このモノリシックなプロジェクトは今もなお健在です。しかし、技術的負債は徐々に積み上がり、保守性の確保や新機能の追加が少しずつ難しくなってきています。

そこで、API専用のアプローチを取り入れ、最新のRailsバージョンで再構築することを検討しています。将来的には、PythonやGolangなど、他のプログラミング言語も活用し、マイクロサービスアーキテクチャへの移行も視野に入れています。ただし、このような大規模な変更を行うには、慎重さが求められます。顧客が増える中で、短期的には現在のRailsプロジェクトを改善し、保守性を向上させる必要があります。

今回は、RailsプロジェクトにおけるService Objectsの重要性とその実装方法について詳しくお話しします。

Service Objectとは何か?

Service Objectは、MVC(Model View Controller)アーキテクチャの中で、通常コントローラやモデルに混在しがちな複雑なビジネスロジックを独立させるデザインパターンです。以下のポイントで、Service Objectを採用するメリットを整理してみました。

  • ビジネスロジックのカプセル化: Service Objectsは、コントローラやモデルを煩雑にする複雑なビジネスロジックをカプセル化します。これにより、コードベースがクリーンで保守しやすくなります。
  • 単一責任の原則: 各Service Objectは単一の機能に責任を持ち、コードをよりモジュール化し、理解しやすくします。
  • 再利用性: ビジネスロジックをService Objectsに抽出することで、アプリケーションの異なる部分でこれらのオブジェクトを再利用でき、DRY(Don’t Repeat Yourself)原則を促進します。
  • テストの改善: Service Objectsは特定のビジネスロジックの単体テストを容易にし、アプリケーションのテストカバレッジと信頼性を向上させます。

なぜRailsでService Objectsを使用するのか

Railsはそのシンプルさと使いやすさで人気ですが、プロジェクトが成長するにつれてコードが肥大化し、保守が難しくなることがあります。Service Objectsを導入することで、以下の利点が得られます。

  • 関心の分離: Service Objectsはビジネスロジックをコントローラやモデルから分離し、アプリケーションをより整理しやすく管理しやすくします。
  • コードの明確化: 複雑なロジックをService Objectsに分離することで、コードが読みやすく、保守しやすくなります。コントローラやモデルは主な責任に集中できます。
  • モジュール性: Service Objectsはモジュール性を促進し、特定の部分の変更を他の部分に影響を与えずに管理できます。
  • 再利用性: Service Objectsはアプリケーションの異なる部分で再利用でき、コードの重複を減らし、DRY原則を促進します。
  • テスト可能性: ビジネスロジックをService Objectsに分離することで、単体テストが容易になり、アプリケーションのテストカバレッジが向上し、各機能が正しく動作することを保証します。
  • スケーラビリティ: アプリケーションが成長するにつれて、Service Objectsはコードベースを整理し、モジュール化するのに役立ちます。これにより、新機能の追加や既存コードの保守が容易になります。

Callable Servicesの実装

数年前から私たちはService Objectsを活用してきましたが、最近はさらにモジュール性とテスト可能性を高めるために「Callable Services」というアプローチを採用しています。この手法では、Service Objectが唯一の公開メソッドcallを持ち、明確な単一責任を持つようにします。

たとえば、講義や試験を完了したユーザーに証明書を発行するプロセスでは、データ準備用の3つのService Objectsと、API呼び出し用の1つのService Objectを組み合わせて利用しています。これにより、責任の分離が徹底され、エラーが発生した際のデバッグも容易になりました。

また、私たちはCallableモジュールを実装し、サービスを呼び出す際の人為的なミスを減らす工夫もしています。以下のコードスニペットはそのモジュールの一例です。

module Callable
  extend ActiveSupport::Concern
  included do
    private_class_method :new
  end
  class_methods do
    def call(...)
      new(...).call
    end
  end
end

まとめ

Service Objectsの導入は、Ruby on Railsプロジェクトにおけるコードのモジュール化、保守性向上、そしてスケーラビリティを実現するための強力な手法です。私たちはこのアプローチを通じて、よりクリーンでテスト可能なコードを維持し、プロジェクト全体の品質を向上させています。

もし、こういったチャレンジに興味があり、私たちのチームでエンジニアとして新しい知識を共有しながら成長したい方がいれば、ぜひお話ししたいです。

WisdomBaseの未来を共に築いていきましょう。読んでいただき、ありがとうございました。

(ファット)


English version: Why and how we should Services object in Rails

As we all know, our ShareWis (WisdomBase) project is a 10-year-old monolithic Ruby on Rails project that strictly follows the MVC (Model View Controller) pattern. Thanks to the efforts of our engineering team, the source code remains maintainable, although it is becoming increasingly difficult. We have considered rewriting our monolithic project with an API-only approach and a newer version of Rails (which we have done but not yet delivered to Production), and possibly breaking it into several projects using other programming languages such as Python and Golang in the future. However, as our company grows with many organizational customers, any significant changes should be carefully considered and implemented. Therefore, in short term, we should focus on making our current legacy project more maintainable and less error-prone. To achieve this, we have started writing blogs to share knowledge and enhance our team’s overall abilities and skills. In this blog post, I will explain why and how we should use Service Objects in our Ruby on Rails project (we have actually been using Service Objects for a few years now). Let’s started with what, why, and how.

What is a Service Object in Rails?

A Service Object in Rails is a design pattern that encapsulates business logic that doesn’t naturally fit into the traditional MVC (Model View Controller) architecture. Here are a few key points:

  1. Encapsulation of Business Logic: Service Objects are used to encapsulate complex business logic that would otherwise clutter controllers or models. This keeps the codebase clean and easier to maintain.
  2. Single Responsibility Principle: Each Service Object is responsible for a single piece of functionality, making the code more modular and easier to understand.
  3. Reusability: By extracting business logic into Service Objects, we can reuse these objects across different parts of the application, promoting DRY (Don’t Repeat Yourself) principles.
  4. Improved Testing: Service Objects make it easier to write unit tests for specific business logic, improving the overall test coverage and reliability of the application.

Why We Use Service Objects in Rails

There are several benefits to using Service Objects in a Rails application:

  1. Separation of Concerns: Service Objects help separate business logic from controllers and models, making the application more organized and easier to manage.
  2. Code Clarity: By isolating complex logic into Service Objects, the code becomes more readable and maintainable. Controllers and models stay focused on their primary responsibilities.
  3. Modularity: Service Objects promote modularity, allowing developers to isolate and manage changes to specific parts of the application without affecting other areas.
  4. Reusability: Service Objects can be reused across different parts of the application, reducing code duplication and promoting the DRY (Don’t Repeat Yourself) principle.
  5. Testability: Isolating business logic in Service Objects makes it easier to write unit tests. This improves the application’s test coverage and ensures that each piece of functionality works correctly.
  6. Scalability: As the application grows, Service Objects help manage complexity by keeping the codebase organized and modular. This makes it easier to add new features and maintain the existing code.

How We Should Use Service Objects in Rails

Since we have been using Service Objects for years, all team members should be familiar with their usage in Ruby on Rails. However, I want to suggest some improvements to our current implementation. By adhering to the Single Responsibility Principle, we can implement “Callable Services.” These are Service Objects with only one public method, named call, along with an initializer and any necessary private methods or helpers. Each Service Object should handle a single responsibility and only one single responsibility.

For example, in the process of issuing certificates to users who have completed courses or exams, we use three Service Objects for data preparation and one Service Object for the API call. This ensures clear separation of responsibilities and makes debugging easier when errors occur.

Additionally, we implemented the Callable module to reduce human errors when invoking services. Below is an example code snippet of this module.​

module Callable
  extend ActiveSupport::Concern

  included do
    private_class_method :new
  end

  class_methods do
    def call(...)
      new(...).call
    end
  end
end

Conclusion

In conclusion, adopting Service Objects in our Ruby on Rails project has proven to be a valuable practice for maintaining clean, modular, and testable code. By following the Single Responsibility Principle and implementing Callable Services, we have improved the clarity and maintainability of our codebase. This approach not only helps us manage complexity as our application grows but also ensures that our team can quickly identify and resolve issues. Sharing knowledge through these blog posts is an essential part of our continuous improvement efforts. By enhancing our skills and adopting best practices, we can deliver high-quality value to our project and our customers, making WisdomBase more solid. Thank you for reading, and we hope you find these insights useful.

(Phat)