RoadMovie

write down memos or something I found about tech things

【第1章】 ModelとServiceを紐解く

※ この記事は「Railsのリファクタリングに立ち向かうための教科書」シリーズの第1章になります(2/5)

 【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
★【第1章】 ModelとServiceを紐解く
 【第2章】 ApplicationServiceの導入
 【第3章】 マイクロサービスを見越した実装
 【最終章】 リファクタリングを通してチームを強化していく


今回はModelとServiceを紐解いていき、リファクタリングの方針を定めていきたいと思います。

責務を決める

序章でも書いた通り、まず何よりも大事なのがディレクトリの責務を決めることです。責務が決まっていなければ、どんなクラスやモジュールをどこに配置すれば良いのかがわかりません。常にこの「責務」については頭に入れてリファクタリングを進めていきましょう。それではさっそくServiceとModelについて考えていきます。本題に入る前に再度下記の図を復習して、頭に入れておいてください。 f:id:mr7myself:20200729222711j:plain

Service

序章にも書かせていただきましたが、ModelがFatになってきたからServiceを作ったというケースがRailsプロダクトには非常に多いです。なのでServiceが実際に何をするべき場所なのかがわからないということが起こることになります。私がおすすめするServiceの実装方針は下記の通りです。

1. ステートレスな関数(関数型プログラミングのような形)
2. 命名動名詞

1. ステートレスな関数

ステートレスにすることで、Serviceはオブジェクトとして扱われなくなり、ただのFunctionとして振る舞うようになります。これにより、何をやるのかが明確になります。どういうことなのかは実際のコードを見たほうがわかりやすいと思うので、まずはそちらを示します。例として、売上のトレンドを計算するServiceを考えます。

module SalesTrendCalculator
  module_function

  def recent_years(sales)
    year = Time.now.year
    amount = sales
      .where(year: [year-1, year-2, year-3, year-4])
      .group(:year).sum('amount')
    calculate_point(amount)
  end

  def calculate_point(amount)
    # Point Logic
  end
end

上記はmodule化することによって、外部からinitializeすることを不可能にしています。こちらは見た目上もわかりやすいのですが、calculate_point メソッドが外部から見えてしまうことを嫌うこともあるかと思います。その場合は下記のようにクラス化して対応します。

class SalesTrendCalculator
  def self.recent_years(sales)
    instance = new(sales)
    instance.recent_years
  end

  def recent_years
    year = Time.now.year
    amount = sales
      .where(year: [year-1, year-2, year-3, year-4])
      .group(:year).sum('amount')
    calculate_point(amount)
  end

  private

  attr_reader :sales

  def initialize(sales)
    @sales = sales
  end

  def calculate_point(amount)
    # Point Logic
  end
end

ステートレスにすることを守ることで、責務が1つで何か特定の機能としてのみ動作するレイヤーという位置づけが担保されやすくなります。

2. 命名動名詞

これは明確にServiceクラスのものであること、ステートレスであることがわかるようにするための工夫になります。具体的には xxCreator, xxUploader, xxBuilder などの名前を使うことになります。よくある命名でやめておいたほうがよいのが xxService という名前です。こちらはApplicationServiceを連想しやすいのでおすすめしません。また、可能であれば services というディレクトリ名も、 domain_services などにした方が良いと思います。

Model

さて、アプリケーションにおいて最も重要になのがModelです。Modelはアプリケーションをモデリングするオブジェクトの住処なので、特に気をつけて責務を考える必要があります。Railsで実装している場合、Modelは「何をすべきか」ではなく、「何をすべきでないか」という視点で考えたほうがうまくいくことが多いです。具体的にはServiceがやるべきこと、QueryObjectでやるべきことの2つをModelから剥がすとうまくいきやすいことが多いです。特に後者はActiveRecord継承クラスにおいては非常に有用です。QueryObjectについては下記に詳しいです。
7 Patterns to Refactor Fat ActiveRecord Models - Code Climate

この2つを実行すると、ModelにはDDDでいうEntity(簡単のためActiveRecord継承クラスを指す)やValueObjectのみを残すことができるようになるかと思います。

Entity

Entityはアーキテクチャによって指すところが違ったりしてややこしいのですが、DDDにおいては一意に識別できるものとされています。実際はその限りではないのですが、RailsにおいてはActiveRecord継承クラスとしておくと、一旦の理解としては良いかと思います。ここで、序章で話した単一責務の原則を意識したモデルを作ると、Entityとして相応しいものに近しくなっていくかと思います。「そのオブジェクトがそのメソッドないし操作を知っていていいのか?」ということを自問しながら実装を進めると良いでしょう。

ValueObject(値オブジェクト)

ValueObjectは一意性が不要なものをモデリングする際に有用です。例えば電話番号、氏名などはよくこの対象になるのではないかと思います。 ※ サンプルについては検索すればいくらでも見つかると思いますので省略します

Tips

最初はEntityとValueObjectの2つを抑えておいてリファクタリングを進めることをおすすめします。だんだん慣れてきてより複雑なモデリングが必要になったタイミングでaggregateなどを導入するのが良いでしょう。また、 concern や継承は思い切って使わないという方針を取ってみるのもおすすめします。考えられた上で実装されていれば強力な武器になりますが、大規模なリファクタが必要となるプロダクトではそうではないケースが多く、そうした思い切りも簡潔さのためには非常に有効でしょう。




いかがでしたでしょうか。今回はモデリングのコアになるModelの実装方法と、それを進めるために役立つServiceについても見てきました。次回「【第2章】 ApplicationServiceの導入」では、さらにApplicationServiceの導入について考えたいと思います。ApplicationServiceの導入までできると、かなりReadabilityとメンテナンス性の高いアプリケーションが実装できるものと思います。

それでは!