【第1章】 ModelとServiceを紐解く
※ この記事は「Railsのリファクタリングに立ち向かうための教科書」シリーズの第1章になります(2/5)
【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
★【第1章】 ModelとServiceを紐解く
【第2章】 ApplicationServiceの導入
【第3章】 マイクロサービスを見越した実装
【最終章】 リファクタリングを通してチームを強化していく
今回はModelとServiceを紐解いていき、リファクタリングの方針を定めていきたいと思います。
責務を決める
序章でも書いた通り、まず何よりも大事なのがディレクトリの責務を決めることです。責務が決まっていなければ、どんなクラスやモジュールをどこに配置すれば良いのかがわかりません。常にこの「責務」については頭に入れてリファクタリングを進めていきましょう。それではさっそくServiceとModelについて考えていきます。本題に入る前に再度下記の図を復習して、頭に入れておいてください。
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とメンテナンス性の高いアプリケーションが実装できるものと思います。
それでは!