※この記事は、2024 Speee Advent Calendar8日目の記事です。 昨日の記事はこちら
はじめに
こんにちは。リフォームDX事業部で開発を担当している佐藤です。Speeeには5年在籍していて、現在はリフォームDX事業部というところでプロダクト作りに関わらせてもらっています。
この度、新しいメンバーと共にRails開発のプロジェクトを立ち上げるにあたって、最速でチームの認識を統一していくためのガイドラインを作りました。よく言語化できたので、少し加工した内容を公開しようと思います。
私自身、エンジニア歴は10年以上で、Railsも10年くらいやっています。Speeeでも新規事業の立ち上げ、10年近く運営されるプロダクトの保守、マネージャーなど幅広くやってきました。 さまざまなフェーズのプロダクトに関わる中で、開発する上で大事にしたい原理原則のようなものが見えてきたので、それをガイドラインに反映しています。
今からチームづくりをする立場の人がこの記事を見てたたき台に使ってもらえたら嬉しいです。プロジェクト特有の事情はなるべく削っているので、一般的なRailsアプリの立ち上げで参考にできると思います。
また、あえて価値観やスタンスを白黒ハッキリと示した内容になっているので、共感できるかは別としても、考えるきっかけにはなると思います。
開発基本指針
対象読者
- Ruby on Rails開発チームのリーダー
- 開発の基準作りに迷っている人
前提の要件
以下のような特性を持ったプロダクトを想定しています。
- Ruby on Rails作っていること
- サービスの立ち上げフェーズで、今は小さいが徐々に規模を拡大させていく前提
以下、技術を問わない基本スタンスと、Railsにおける開発指針の二つに分けて示していきます。
基本スタンス
ここでは、私たちが立脚している基本的な考え方を示すことで、価値判断基準を揃えることを目的とする。技術を問わず、価値観として持っているものを以下に示す。
ユーザーに対する価値が大きいこと = 良いことである
当然ではあるが、ソフトウェア開発は社内の誰かの目的を叶えるものではなく、ユーザー対する価値を大きくするために行う。
そのため、雇用形態を問わず、ユーザーへの価値が大きくするために意見を戦わせることは全面的に歓迎される。
技術の良し悪しはユーザーへの提供価値の大きさによってのみ決定可能である
私たちは「顧客特性によって事業特性が決まり、それによって技術やアーキテクチャが決定される」という構造があるという立場に立脚している。
技術判断は本質的には顧客特性(エンドユーザの特性)によって決定されるべきである。ただ実態としては顧客に提供するサービスの事業構造、提供形態によって決まる。
例えば、以下などである。
- サービス特性によって厳密性が要求されたり、アジリティが要求されたり、求められる要件が変わる
- ↑に応じて、採用する技術や、設計や、ルールが変わる
あらゆる技術的な決定にはトレードオフが伴う
アーキテクチャではすべてがトレードオフだ。だからこそ、アーキテクチャのあらゆる問いに共通する答えは、「場合による」のだ。この答えにいら立つ人は多いだろう。しかし、残念ながらそれ が真実だ。REST とメッセージングはどちらが良いか。マイクロサービスは正しいアーキテクチャ スタイルなのか。こういった問いに対する答えを Google で見つけることはできない。それは場合によるからだ。
アーキテクチャに限らず、判断や意思決定が伴うものは全てこの原則が当てはまると考えている。そのため技術的な決定は「その技術を採用するトレードオフは何か?」を考え、良い点と悪い点の双方を出した上で、悪くない選択を選ぶ形になる。
トレードオフが出せない場合は、何かを見落としていると考えた方が良い。
正しい選択ではなく、アップデート可能な意思決定をする
技術的な選択は絶対的に正しい選択というものは存在しない。あくまでその時点での戦略、制約など限定的な情報を前提として決定されるものである。そのため、前提条件が変わった場合、それはアップデートされなければならない。
そのため、当時の意思決定背景を残すことを大切にする。
具体的には意思決定は ADR の形式で文書化し、その時の前提となる背景、意思決定ドライバー(≒目的)、選ばなかった選択肢を残しておくこと。
YAGNI、KISS原則 に則る
今後必要かもしれないものは、今後も必要ない。考えうる最も素朴でシンプルな選択肢を選ぶこと。
特に立ち上げフェーズにおいては将来の見通しが外れることの方が多い。
「将来について考えるが、実際には作らない」くらいが適切。
フレームワークの標準を忠実に守る。逸脱するのは「ここぞ」というとき
我々が作るプロダクトは、多くの部分で標準的なWebアプリケーションであり、フレームワーク(ライブラリ)の想定を逸脱する場面というのは非常に稀である。
そのため、原則としてはフレームワークの標準、Webの標準をよく理解し、忠実に守りながら開発するべきだ。
逸脱が求められるケースは、このプロダクトが独自の戦略をとるという意味なので、慎重さと覚悟を持って選ぶことになる。せいぜいPJで一つだろう。
経験則的にも戦略に適合しない独自実装は中長期的に見ると生産性を下げることの方が多い。
公式ドキュメントを丁寧に読んだら解決できることは、公式ドキュメントを丁寧に読んで解決するべきである。
社内の先輩リポジトリを積極的に模倣すること
もしも、社内に似たようなプロジェクトがあるなら積極的に模倣した方が良い。
社内のプロジェクトというのは
- そもそも参入している事業ドメインやビジネスモデルが似通っていることが多い
- 自分たちよりも年数が経っていたり、先に問題にぶつかっていることが多い
ので、問題解決をショートカットできる。
我々が直面する問題の大半は別プロジェクトで解決済みで、社内を探れば何かしら見つかると思った方が良い。
Rails開発指針
以降は、Rails開発における基本的な価値観やスタンスを示す。
Railsの公式のベストプラクティスに忠実に則ること
まず前提として、私たちはユーザに価値のあるコードを書く時間を最大化したい。それ以外の時間は可能な限り圧縮したい。
標準から外れる場合、それを説明、浸透、維持する責任とコストが発生する。複数の人間が共通認識を形成し続けるのは想像以上に大変なことである。
公式なベストプラクティスに則っている限りは最低限のコストで共通認識を形成でき、その分コミュニケーションコストを減らすことができる。
特にRuby on Railsはベストプラクティスがほぼ出揃っているので、積極的に枯れたコンセプトに乗っかっていきたい。
Railsは公式のRailsWayに準拠。新しいディレクトリを生やしてはならない
いわゆる7パターンのようなディレクトリも生やしてはならない。
作った時に便利に見えても、中長期的に見た場合デメリットの方が大きいと考えているためである。
大抵の問題はモデルを丁寧に作ることで解決できる。ディレクトリが分かれるとコードが散在してかえって見通しが悪くなる。 経験値的にも一貫性を守りきれず管理できなくなることの方が多い。例えば「このロジックは/modelsに置きますか?それとも/servicesに置きますか?」などの無意味な議論を誘発してしまう。
素のRailsは十分に豊かである という立場に立って、Railsの仕組みの中でうまくドメインモデルを組み立てる方法を模索してほしい。
ただし、concernsとcallbackは自信がなければ使わなくてもいい
前段と真逆の話をしてしまうが、concernsとcallbackの利用には十分な注意を払ってほしい。 鋭利なナイフを開発者に提供するスタンスをとるRailsだが、この2つは特に鋭利な機能なためだ。
concernsはGood concernsで紹介されるように、上手に使えば表現力の高いモデルを生み出せる一方で、使い方を誤るとひどい混乱をもたらすコードにもなり得る。(こういったツラいコードを何度も見てきた)
callbackも同様で、使い方を誤ると何が起きるか予測不能な危険なコードを生み出すことができてしまう。
使わないわけではないが、強力さゆえの危険性を持っている認識を持って扱ってほしい。
名前空間を積極的に使って、コードを階層化すること
このプロダクトは成長を目指しているので、最初からコードの認知負荷を減らすための工夫を入れていく必要がある。
たとえば10年運営されるようなプロダクトだとモデルの数は数百まで増えたりして、人間が認知できる限界を容易に超えてしまう。そのため管理可能なレベルに階層を切りながら育てていきたい。
アーキテクチャレベルでモジュールを分けるような選択肢もあるが、原則として採用しない。アーキテクチャの分離は不可逆な意思決定を伴うのでリスクが大きく、モノリシックRailsで綺麗に作れるならそれに越したことはないので、まずはモデルを綺麗に作ることを考えてほしい。
モデルを丁寧に作ること、特にActiveRecordモデル
ドメインモデルを丁寧に作ることが重要なのは言うまでもないが、特にActiveRecordモデルはデータも作られる後からの変更が大変になる。そのため、雑な設計は避けて、多少の時間をかけてでも丁寧にモデルを作ってほしい。変更コストを加味するとそのコストは回収できる。
一方で、POROなど後から容易にリファクタリング可能なものは神経質にならなくて良い。こちらは、多少の歪さが残ったままリリースしても構わない。
テーブル設計では制約をちゃんとつけること
NOT NULL制約、外部キー制約、ユニーク制約などの制約は、原則として漏れなく付けること。 データベース上で整合性を保てないと、アプリケーション側のロジックが複雑になったり、運用に負荷がかかったりする。
経験上も、データベースの整合性が緩い結果、過去データが不整合を起こしていたり、存在しないはずのデータが実はあるケースをよく見てきた。
とりあえず厳しめに設定して、問題があれば外すくらいのスタンスで構わない。
イミュータブルデータモデルになるべく則ること
データモデリングに関しては、「イミュータブルデータモデル」のコンセプトに大きく影響を受けている。
ここでは深く触れないが、
- イベントエンティティを上手に取り出すこと。それが表現力の高いモデルを作る鍵になる
- レコードのUPDATEはイベントエンティティをうまく抽出できていないサインである
と解釈しているので、レコードのUPDATEに注意を払いつつ、イベントエンティティを上手に取り出すことを意識してデータモデリングしてほしい
以上。
おわりに
このガイドはある前提条件でのみ有効で、今後もずっと正しいものとは思っていません。おそらくプロダクトの数だけ開発ガイドがあるのでしょう(No one paradigmですね)。
批判的なご意見お待ちしています。(私の経歴的に立ち上げが多く大規模システムの経験がないので、そういう視点が欠けている気がしています。)
偉そうなこと書いてますが、私もまだまだ未熟なエンジニアだし、組織もまだ伸び代ばかりです。若いリーダーを支えるベテランも、次世代のリーダーである新卒もビジョンに対して全然足りない状態です。 真面目に技術と向き合っている会社である自負はあるので、もし興味を持ってもらえたら是非カジュアル面談でも選考応募でもいただけると嬉しいです。
他にも様々なポジションで募集中なので「どんなポジションがあるの?」と気になってくれてた方は、こちらをチェックしてみてください。
おまけ
せっかくなのでチームで採用しているコーディングルールも置いておきます。今のプロダクトはGraphQLを採用しているのでGraphQL Rubyのルールも含みます。その代わりViewやControllerのルールはないです。
本当はReactのルールもあるのですが、そちらは作成途中なのでまた今後で。
モデル
ディレクトリ構造
- /app配下におけるディレクトリは、以下のみである
- Railsが標準で提供するディレクトリ(models/controllers/views/jobsなど)と
- graphql-rubyが提供するディレクトリ(/graphql)
- モデルは全て/app/models 配下に置く。/servicesなどは作ってはならない
モデルの構造
利用するモデルが複数に渡る場合、クラスの名前空間内部にインナークラスを定義しても良い。モデルのネストが深くなることは特に気にせず、階層化されるメリットの方が大きい、というスタンスをとっている。
- foo/ - bar.rb # Foo::Bar - baz.rb # Foo::Baz - foo.rb # Foo
ActiveModelは積極的に作って良い。
名前空間の階層構造を作るか?の基準
厳密な定めはないので、その都度会話して決める。
階層構造を作るケースは以下のようなケースが該当する。
- 本来的にはそのテーブルの属性であるがActiveRecordモデルとしては分かれている場合
User::PhoneNumber
- 特定の要素技術に関するコードをまとめておきたい場合
Firebase::
AWS::
- 特定の処理を実現するPOROを作りたい場合
Account::Closing::Purging
原則としては積極的な階層化を推奨しているので、「コードが大きくなってきたらとりあえずインナーモデルに切り出す」くらいでも良い。特にPOROは気軽に切り出して良い
階層間でのモデルの参照ルール
名前空間の外側から、ネストを辿ってモデルをインスタンス化することは、あまり推奨されない。 内部モデルの複雑さが露出することで呼び出し側に負荷がかかるためである。
NG
Recording::Incineration.new(recording).run
OK
recording.incinerate
ただし、これを厳密にやるのは無理なので、なるべく意識して欲しい程度である。
Concernsの置き場
- app/models/concernsに置く
- 利用している範囲を限定するために、モデル同様に名前空間を切ること
DBの命名ルール
- Railsの命名ルールに則ること
- 特に予約語的な名前がいくつかあるので、被らないように命名すること。
type
という単語は避ける - 日時を表すカラム名の命名は 過去分詞形 + _(at|on) とする。
- 利用開始日 (date) の場合: started_on
- 作成日時 (datetime) の場合: created_at
ActiveRecord::Rollbackについて
raise ActiveRecord::Rollback
は原則として使用しない。ネストされたトランザクション内で上記を呼び出しても、ロールバックされないなど、挙動にかなり癖があるため。
ネストされたトランザクションをロールバックしたい場合は、これ以外の例外を投げる
参考情報:
- https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#:\~:text=ActiveRecord%3A%3AStatementInvalid occurred.-,Nested transactions,-transaction calls can
「Kotori」と「Nemu」の両方を作成します。理由は、ネストされたブロック内の ActiveRecord::Rollback 例外が ROLLBACK を発行しないためです。これらの例外はトランザクション ブロックでキャプチャされるため、親ブロックはそれを認識せず、実際のトランザクションがコミットされます。
モデルや名前空間の名前がgemで提供される名前空間と重複する場合
- 先頭に
My
をつける。 - 被るgemがなければMyは不要。gemと被ったらMyをつける
GraphQL
QueryType
- ルートであるQueryTypeに生やしたfieldは別途リゾルバを定義してリゾルバを指定すること
- それ以外のTypeに関してはメソッドを生やす形でのリゾルバ定義で構わない。特にルールはない
- Typeは末尾に
Type
をつける。ActiveRecordモデルとの衝突を防ぐため - 名前空間はモデルと大体合わせて階層化する(厳密には決まってない)
- リレーションのfieldを呼び出すときはN+1回避のためにAssociationLoader経由で呼び出すこと
Mutation
- 冪等性を担保すること。つまり、同じ内容で同じmutationが2度呼ばれても期待通り動作すること。2回呼ばれたときのテストを必ず追加すること。
- フラットに並ぶと管理が難しくなるので、適切な粒度で名前空間を切ってグループ化しても良い。グルーピングの基準は特に決まってない
テスト
RSpecの記述ルール
- 基本的にdescribeやcontext, itの説明文は、可能な限り日本語で記述すること
- キーワード系は以下のルールで記述する
- インスタンスメソッド:
"#メソッド名"
- クラスメソッド(スコープを含む):
".メソッド名"
- インスタンスメソッド:
その他
一度しか使わないスクリプトの扱い
一度しか使わない想定のスクリプトは、一度gitにpushしてコミットログで追えるようにした後、利用後に落ち着いたら削除する。こういったコードを残しておくと、Railsアプデなど横断的に修正を行いたい場合に引っ掛かって邪魔になるため。
- rakeタスクでスクリプトを作成し、lib/tasks/once/{yyyy} に配置する