※この記事は、Speee Advent Calendar22日目の記事です。 昨日の記事はこちら tech.speee.jp
2021年7月から業務委託のエンジニアとして主にイエウールの開発のお手伝いしている高尾です。所属は株式会社ネットワーク応用通信研究所。Rubyのまつもとゆきひろさんも在籍されており、Rubyに関するSIでそれなりの実績のある会社です。私は20年近くSIerとして仕事をしてきました。
そんな私にとってもSpeeeでの開発は魅力的です。
プロジェクトの運営、プロダクトの仕様、技術の採用、リリースなど、多くのことをエンジニアが主体的に決めます。各エンジニアがお客様の価値を理解してプロダクトを作り上げるという意識が伝わってきます。そんなエンジニアのみなさんが、 Rubyをつかって楽しくプログラミングできるように全力でサポートしていきたいと思います!
前置きが長くなってしまいましたが、今回は Ruby on Rails を使って開発しているイエウールで導入している Rails のデザインパターンを、私の独断で
- Keep = 今後も積極的に使っていくもの
- Problem = 問題があるためメンテナンスのみで今後は使わない
に分類して紹介します。
また、 Keepに対するTryとして、より良くするために今後やっていきたいこと を書きます。
なお、イエウールは2014年にサービスを開始し、2016年にRuby on Railsでリプレイスしています。そのときの最初のコミットは2016-01-25でしたので、 Railsでの開発期間は約6年間 です。 *1
それでは各デザインパターンをファイル数の多い順に紹介していきます。
Serviceオブジェクト (Keep)
最初は Service オブジェクト と呼ばれているパターンです。
このパターンについては説明は Railsで重要なパターンpart 1: Service Object(翻訳)|TechRacho by BPS株式会社 が詳しいです。
イエウールでは app/services 以下に 140 ファイルあり、最も多く使われているデザインパターンです。
- 複数のモデルを操作する処理
- メールやSlackへの通知
- バッチの処理
- その他、分類が難しいもの
を実装するのに使っています。
オブジェクト指向を意識せずに、やりたいことを素直に実装できる ため、開発初期からずっと使われ続けています。
ただし、例に漏れず Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)|TechRacho by BPS株式会社 に挙げられている問題点がそのままイエウールにも当てはまってしまいました。
本質的にService Objectパターンそのものには、コードベースを読みやすくする力も、メンテしやすくする能力も、concernをうまく分割する手腕もありはしない
そうなんです。 Service オブジェクトを実装した人にとっては短い時間で簡単に実装できたかもしれませんが、メンテナンスする人にとっては再利用しにくく、自動テストもやりにくいものになっています 。
Service オブジェクトパターンは今後も使っていくのですが、Tryとして
- (1) public なメソッドは call のみとする
- (2) モデルに実装すべき処理を積極的にモデルに実装する
ということを気をつけていきたいと考えています。
なお、(1) を実現するために Selleo/pattern: A collection of lightweight, standardized, rails-oriented patterns.を参考にして ApplicationService クラスを用意しました。今後はこれを継承して Service オブジェクトを実装していきたいと考えています。
class ApplicationService class << self def call(*args) new(*args).call end ruby2_keywords(:call) if respond_to?(:ruby2_keywords, true) end # :nocov: def call raise NotImplementedError end # :nocov: end
Decoratorオブジェクト (Keep)
次は Decorator オブジェクト と呼ばれているパターンです。
Decoratorオブジェクト | Railsのデザインパターンまとめ - applis で解説されています。
イエウールでは、特定のビューのみで使うヘルパーメソッドを定義するためにこのパターンを使っています 。Rails の Helper だとメソッド名が重複しないように気をつける必要があるんですよね。
Decorator オブジェクトは draper gem をつかって実装し、app/decorators
に配置しています。
数えてみると 59 ファイルあります。結構多いです。
気軽にヘルパーメソッドを定義できて便利なので今後も Decorator パターンを使っていくのですが、Tryとして、モデルとの関連がない Decorator オブジェクトがいくつかあるため、それらをリファクタリングしていきたいです。
実際のコードではありませんが、具体的な例を挙げると、
# UserモデルとDecoratorオブジェクトの関連がなく、 # ユーザーが所属する企業名を返すヘルパーメソッドcompany_nameの # 引数にUserモデルのインスタンスを指定している class UserDecorator class << self def company_name(user) if user.company.parent_company.present? return "#{user.company.parent_company.name} #{user.company.name}" end user.company.name end end end
というコードを以下のようにリファクタリングしたいということです。
# これがDraperの本来の使い方 # UserモデルとDecoratorオブジェクトを関連させて、 # company_nameメソッドでUserのインスタンスメソッドを呼び出している class UserDecorator < Draper::Decorator delegate_all def company_name if company.parent_company.present? return "#{company.parent_company.name} #{company.name}" end company.name end end
Formオブジェクト (Keep)
次は Form オブジェクト と呼ばれているパターンです。
1つのフォームで複数のレコード、複数のモデルを扱うようになると accepts_nested_attributes_for
を使うことになります。が、このメソッド、少し古いですが以下のような記事があり、評判がよくありません。
- Railsのaccepts_nested_attributes_forについて解説してみた。 | 目指せ、スーパーエンジニア
- accepts_nested_attributes_forを使わず、複数の子レコードを保存する | Money Forward Engineers’ Blog
イエウールでは、Form オブジェクトパターンを使って、1つのフォームで複数のレコード、複数のモデルを扱っています。
app/forms
に配置していて 28 ファイルあります。
PHPから移植したフォームもあり、そういったものにはこのパターンがとても有効 です。
Form オブジェクトパターンも今後も使っていきたいのですが、Tryとして、独自実装となっているため、 pattern/form.rb at master · Selleo/pattern を参考にして、今後は以下のような実装にしていきたいと考えています。
# app/forms/application_form.rb class ApplicationForm include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Validations def initialize(attributes: nil) attributes ||= default_attributes super(attributes) end def id nil end def persisted? false end def save valid? ? persist : false rescue ActiveRecord::ActiveRecordError => e Rails.logger.error([e.message, *e.backtrace].join($/)) errors.add(:base, e.message) false end private def default_attributes {} end def persist true end end
Valueオブジェクト (Keep)
次は Value オブジェクト と呼ばれているパターンです。
Railsのデザインパターン: Valueオブジェクト - applis で解説されています。
イエウールでは、例えば面積を表す Value オブジェクトで単位を平米や坪に変換する、といった使い方をしています 。
app/value_objects
に配置していて 18 ファイルあります。
Value オブジェクトについては今後も使っていきたいですし、特に Try はありません。
強いてあげるならば、歴史的な経緯で実装に virtus gem を使っているため、ActiveModel で置き換えたいですね。
Repository (Problem)
最後は、 Repository パターン です。
Repositoryパターン | Railsのパターンとアンチパターン2: モデル編とマイグレーション(翻訳)|TechRacho by BPS株式会社 で解説されています。
Problem としていますが誤解のないように先に説明しておくと、 Repository パターンが問題なのではなく、不要になったので削除するということです。
イエウールでは、住所マスターの更新にともない、それまではDBに直接格納していたものを、外部DBやAPIサーバーから取得できないか検討したことがあります。その過程で、Repository パターンを使って住所マスターのDBをラップした Addresses::Repositories::Address
といったクラスを定義しました。
配置場所は特殊で app/addresses/addresses/repositories
です。7 ファイルほどあります。
ただし、結局、住所マスターはDBに直接格納することになり、住所マスターのDBをラップしたクラスは不要になり、今後削除していくことになりました。
まとめ
ある程度の規模になるとRailsの標準的なMVCだけでは、コントローラーやモデルが肥大化してしまい、メンテナンスがやりにくくなります。イエウールも今回紹介した5つのデザインパターンを導入して、その問題に対処してきました。特に Service オブジェクトパターンは、複数のモデルが関係する煩雑なビジネスロジックの実装に有効 だったと考えています。
また、Try を考える過程で、 デザインパターンは導入すればいいというものではなく、それらの目的や責務をしっかりと理解した上で実装しないといけない 、ということをあらためて感じました。そうしないと Service オブジェクトのようにメンテナンスや再利用が難しいものになってしまいます。
これからもイエウールは進化していきます。
機能追加と並行して、今回挙げたTryも実現して、よりメンテナンスしやすく再利用できるコードにしていきたいと考えています!
おわりに
Speeeでは一緒にサービス開発を推進してくれる仲間を大募集しています!
もしSpeeeに興味を持っていただいた方は以下で社内メンバーのカジュアル面談を公開しているので、お気軽にご連絡ください💁
Speeeでは様々なポジションで募集中なので、「どんなポジションがあるの?」と気になってくれてた方は、こちらチェックしてみてください。
もちろんオープンポジション的に上記に限らず積極採用中です!
*1:イエウールのサービス開始は2014年ですが、初稿では「イエウールの最初のコミットは2016-01-25でしたので、これまでの開発期間は約6年間です。」と記載しており、誤解を招く表現となっていました。お詫びして訂正いたします。