Rails:Service層を運用して良かったところ、悪かったところ

  • 312
    いいね
  • 1
    コメント

1年前くらいにRailsの設計にDDD(ドメイン駆動設計)のService層を導入し、Modelの肥大化対策をしました。
この記事では、まずどのようなルールでService層が組み込まれているかと、1年間運用してみて良かったところ、悪かったところの感想を書きます。

Service層を導入するきっかけになった問題点

  • Modelの肥大化
  • Model間の複雑な依存関係
  • 多数のミドルウェアの導入による複雑さの倍増

これらにより..

  • メンテナンスやテストがしにくい
  • コードが整理されていないのでとにかく読みづらい

Model複雑化の例

<ユーザがECサイトの商品をお気に入り(like)にするメソッドを書く場合>

処理に関連するテーブル

  • my_itemsテーブル(like情報を持つテーブル)
  • itemsテーブル

処理に関わるミドルウェア

  • MySQL
  • Redis

必要な処理

  1. my_itemsテーブルへのinsert
  2. itemsテーブルのlike_countの更新
  3. ユーザがlikeしたアイテムのidを保持するRedisの更新
  4. アイテムをlikeしているユーザのidを保持するRedisの更新

上記のような処理をModel内の1つのメソッドに書こうとすると、他のModelや複数のミドルウェアのupdateを全てをMyItem Modelに記述することになり、Modelが煩雑化してしまいます。一方で、上の処理をControllerに書こうとすると、アプリケーション内でlikeの処理を統一できなくなります。(method化できない)

そこで

ControllerとModelの間にService層を導入

Service層とは?

Service層とは、DDDのドメイン層の構成要素のひとつです。
DDDは下記のように構成されていて、MVCとの対応関係はこんな感じです。

ddd_mvc2.png

Service層は、エンティティや値オブジェクトに属さない層で、振る舞いだけを記述する層です。

MVCではService層がないため、単体のModelで完結しない処理を無理矢理ControllerかModelに詰め込むしかなかったですが、Service層を入れる事で、単一Modelで完結しない処理を一つの処理として切り出して記述することができます。

実際の設計

Service層の定義(ルール)

Service層を作成するにあたって、今回は下記のルールに基づいて設計してあります。

  • 一つのModelで複数のミドルウェアと通信する場合はService層に書く
  • 複数のModelが絡み合う処理はService層に書く
  • Service層では振る舞いだけを定義する(状態を持たないためModuleで設計する)

ディレクトリ階層

app/
  controllers/
    my_item_controller.rb
  services/
    my_items/ # ←名前空間が被らないように複数形にする
      post_service.rb
      search_service.rb
  models/
    my_item.rb
    item_like_user_index.rb (Redis/cache処理)
    user_like_item_index.rb (Redis/cache処理)
  • 各Modelに紐づくService層を用意
    • (例だと services/my_items が models/my_item.rb に紐づいている)
  • Service層の中身を下記の二つのファイルに分類
    1. post系
    2. search系

Service層では、例えば"商品をlikeする"や"商品を購入する"等の行動単位でModule化することが多いところを、今回は行動単位ではなく、各Modelに紐づくService層をpost, search系の処理に分けてModule化しています。記述したい行動と、より密接に関わるModelのService層に処理を書くルールとなっています。
行動単位にしない理由は、

  1. Moduleが乱立することがなくディレクトリが奇麗
  2. Model単位で分けるとどこになにを書くのかが直感的でわかりやすい

の二つがあります。

ECサイトの商品をお気に入り(like)にする場合の例

service_layer.png

実装例

スーパー簡略バージョンです

my_item_controller.rb
  def create
     MyItems::PostService.like!(user_id, item_id)
  end
post_service.rb
module MyItems
  module PostService

  module_function

   def like!(user_id, item_id)
      MyItem.new.set_myitem(user_id, item_id)
      Item.find(item_id).increment_like_count
      ItemLikeUserIndex.new(item_id).liked_by!(user_id,at)
      UserLikeItemIndex.new(user_id).like_to!(item_id,at)
   end

  end
end
my_item.rb
class MyItem < ActiveRecord::Base

  def set_myitem(user_id, item_id)
    self.user_id = user_id
    self.item_id = item_id
    self.save!
  end

end
item.rb
# itemテーブルが保持するlike_countカラムの更新
class Item < ActiveRecord::Base

  def increment_like_count
    self.increment!(:like_count)
  end

end
item_like_user_index.rb
# Redisの各itemをlikeしたuserリストを保持する処理
class ItemLikeUserIndex

  def initialize
    @item_id = item_id
  end

  def liked_by!(user_id, at)
    @@redis.zadd(_key, at, user_id)
  end

end
user_like_item_index.rb
# Redisの各userがlikeしたitemリストを保持する処理
class UserLikeItemIndex

  def initialize(user_id)
    @user_id = user_id
  end

  def like_to!(item_id, at)
    @@redis.zadd(_key, at, item_id)
  end

end

他のModelの処理やミドルウェアの更新等がMyItem Modelから切り出されることにより、MyItem Modelの煩雑さはなくなり、肥大化を抑えられた感じがします。

運用してみて感じた事

メリット

  • ModelやControllerがシンプルになり、読みやすい
  • postとsearchに分けてあるとコードが整理されて、どこに何が書いてあるかわかりやすい
  • 処理がservice層に切り出されている為、Modelのテストが書きやすい

デメリット

  • チームで認識が揃っていないと運用が難しい (サービスの役割とモデルの役割について人によって解釈が違うと、どこになにが記述してあるか逆に分からなくなる)

結論

チーム開発においてService層を導入する際は明確で理解しやすいルールを用意することが重要だと感じました。
今回の設計ではルールが分かりやすくなっているので運用で迷う事が少ないく、チームの認識のずれも生まれにくかったです。
また、Service層が、Modelに紐づいたModule設計になっているので、Service層のディレクトリが奇麗にまとまっていて、どこになにが記述してあるか分かりやすいです。
Modelに紐づいたService層にすることで、ある行動をどのModule内に記述していいかわからない場面があるかと思いましたが、今のところは特に困る事はなく実装できています。