概要
大規模なRailsアプリケーションでModelを作成するとき、既存の類似したModelと名前が衝突するのを避けたり、Modelの置き場所を整理したりするためにmoduleを切ることがあります。今回は開発中にいただいたコードレビューから、なぜmoduleで名前空間を切るのがよいのかを考えます。
遭遇した問題
最初の実装
私が現在ジョインしているプロジェクトでは、1機能を表す複数ののModelを一つの名前空間とディレクトリにまとめることでModelを整理しています。実装したModelはDBに実装した1テーブルを表すschemaファイルに連動しています。
models
├ nice_feature # 名前空間に応じたディレクトリ分け
| ├ boat.rb
| └ yacht.rb
|
├ other_model.rb
...
db/schema
├ nice_feature_boats.schema # ファイル名はテーブル名に対応している
└ nice_feature_yachts.schema
テーブルのほうでは、Modelの名前空間を意識して nice_feature_ というプレフィックスをつけてテーブルのスキーマを宣言しています。Modelのほうでは、そのプレフィックスがディレクトリ名となっています。
問題となるのは、Modelのファイル内におけるクラスの宣言方法です。
最初に私が書いたクラス宣言方法は下記のようになっていました。
class NiceFeature::Boat < ApplicationRecord # なんとなく名前空間を全部含めたほうが探しやすそう
self.table_name = 'nice_feature_boats' # このままだとテーブル名がboatsとして認識されちゃうから書き換えちゃえ
has_many :screws
belongs_to :private_property
...
end
この時の私が考えていたことは下記のようなことでした。
- 「とりあえず後からエディタで探しやすいようにしたいな」
- 「なんかテーブル名にプレフィックスがつかない(ActiveRecordはboatのところしかテーブル名としてみてくれない)からテーブル名はカスタムにしちゃえ」
とかそんなことを考えていました。
そして意気揚々とコードレビューに出すと、レビュワーの方から次のような指摘をいただきました。
クラス名は必ず名前空間用のmoduleを切ってください!
毎回self.tableを指定するのは大変です。
それに、このままだとテーブル名がModel名とずれてもわからなくなってしまいます!
確かに、テーブル名と対応するモデルクラスをすぐに見つけられるように、なるべくActiveRecordがテーブル名を推測してくれてself.tableを使わなくていいようなクラス名にするのは可読性のために必要なことでした。
修正内容
コードレビューに対して私は下記のような修正を加えました。
ディレクトリ構成
models
├ nice_feature
| ├ boat.rb
| └ yacht.rb
├ nice_feature.rb # 名前空間を切る用のファイルを作成
├ other_model.rb
...
db/schema
├ nice_feature_boats.schema # ファイル名はテーブル名に対応している
└ nice_feature_yachts.schema
boat.rbの内容
module NiceFeature
class Boat < ApplicationRecord # プレフィックスのおかげでテーブル名と対応できる
has_many :screws
belongs_to :private_property
...
end
end
nice_feature.rbの内容
module NiceFeature
def self.table_name_prefix
'nice_feature_' # この名前空間に属するモデルクラスのテーブル名にはすべてこれがつく
end
end
これで、 nice_feature ディレクトリの中にあるModelの名前が自動的にテーブル名と対応できるようになりました。
学び
なぜmoduleを使って名前空間を切るのか
この経験を経て、RailsのModelでmoduleを使って名前空間を切ることの利点を考えてみました。
その理由は下記2つだと思います。
- ほかのドメインで使われる似た概念との混同や名前衝突を防ぐことができる
- テーブル名のプレフィックスと併用することで命名規則を遵守させられる
別ドメインの混同や名前衝突の防止
混同や名前衝突の防止(参考1)(参考2)については、下記のような例があると思います。例えばToBとToCの両方に展開しているWebサービスでは、B向けのアカウントとC向けのアカウントを完全に分けていたとします。どちらもアカウントは User ですが、これら2つの概念は似て非なるものなので混同させたくありません。
そのようなときには、B向けのアカウントは Client 、C向けのアカウントは Consumer として名前空間を分ければ、両者の違いは一目でわかるようになります。名前の衝突はしません。ついでに名前空間に応じてディレクトリも分ければさらにわかりやすくなります。
models
├ consumer
| └ user.rb # Consumer::User
├ client
| └ user.rb # Client::User
├ consumer.rb # module Consumer
├ client.rb # module Client
└ client.rb
テーブル名のプレフィックスによる命名規則の遵守
moduleでテーブル名の規則をまとめておくことで、命名規則をコード全体で遵守しやすくなります。
module内に作られたクラスの場合、ActiveRecordは自分のクラス名までしかテーブル名とみなしてくれません(参考)。そのため、今回のようにmoduleでクラスを囲うときにはテーブルのプレフィックスを定義しなければなりません。
moduleでプレフィックスを付けない場合、テーブル名の名前衝突を避けるには、モデルクラス内で明示的にself.table_nameを定義することを余儀なくされます。しかし、それでは同じmoduleに属するモデルクラスにすべてself.table_nameを定義する必要が出て、Railsの基本原則であるDRY原則に反します。コードが冗長になると、タイポなどによってテーブル名が一致しなくなるリスクもありますし、テーブル名の修正漏れが出るリスクも大きくなります。
所感
動くコードを書くことは昨今の時代あまり難しくなくなってきましたが、動くコードのなかでも、どうやって設計・実装するかで実装物の良しあしが決まってくる気がします。それを判断するために、何が、どうしてよいのかを自分で理解したいです。そのためには、なるべく信頼できる情報を自分で調べるのは必要だと思いました。