シンプルにやろう
はじめに
言いたいことはこれだけなのですが少し解説していきます。
- シンプルにやることとシンプルなクラスを作ることはイコールではない
- フレームワークや言語の流儀に無理に逆らうのは茨の道
- フレームワークなどで定められたやり方( Rails way, etc... )以外をしないわけでもない
解説は Ruby と Rails を使ったコードになります。
言語やバージョンに強く依存するものではないですが Ruby 3.3 と Rails 8.0 くらいを想定してください。
シンプルにやることとシンプルなクラスを作ることはイコールではない
以下のどちらの例がシンプルに感じますか?
class Foo1
include ActiveModel::Validations
attr_accessor :num
validates :num, numericality: { only_integer: true, in: 1..100 }
end
class Foo2
attr_accessor :num
def valid?
num_policy = NumPolicy.new(num)
num_policy.comply?
end
end
class NumPolicy
def initialize(num)
@num = num
@rules = [NumIntegerRule.new(@num), NumRangeRule.new(@num)]
end
def comply?
@rules.all?(&:ok?)
end
end
class NumIntegerRule
def initialize(num)
@num = num
end
def ok?
/\A[+-]?\d+\z/.match?(@num.to_s)
end
end
class NumRangeRule
def initialize(num)
@num = num
end
def ok?
(1..100).cover?(@num.to_i)
end
end
クラス単体で見ると「例 2 」は Active Model に依存しない PORO の組み合わせでシンプルと言えるかもしれません。
しかし、一般的な Rails ユーザーであれば「例 1 」の方がやりたいことが自明でシンプルに感じるのではないでしょうか?
例が単純すぎるため「例 2 」でやる意味が感じられないかもしれませんが、もっと複雑な条件になってくると「例 2 」のような実装が生きてくるかもしれません。
また、「例 2 」のように Policy クラスを導入する場合でも、ルールが大量にないのであれば以下の「例 3 」のように Rule クラスを作らずに Policy クラスのみを使う方式が見通しが良い場合もあるかもしれません。
class NumPolicy
def initialize(num)
@num = num
end
def comply?
comply_num_integer_rule? && comply_num_range_rule?
end
private
def comply_num_integer_rule?
/\A[+-]?\d+\z/.match?(@num.to_s)
end
def comply_num_range_rule?
(1..100).cover?(@num.to_i)
end
end
業務のコードではそれなりに扱う条件が多くなったりします。シンプルなクラスに分けすぎたがゆえにコードを追うのが大変になってしまっているケースも見受けられます。
神クラスみたいなものは論外としても、必ずしもクラスに分ける必要はなくメソッドに分けるだけで十分な場合もあり、状況によって使い分けていくのが大切だと思っています。
フレームワークや言語の流儀に無理に逆らうのは茨の道
いくつか例を挙げてみます。
フレームワークの規約に従わない
Rails では規約に従うことで少ないコード量で最大の成果を得られるようになっています。
簡単な例で言えばディレクトリー構造や命名規則です。
これに従わないことで組み合わせた gem がうまく動かなかったり、多くのコードを自前で用意する必要が出たりします。
もちろんデメリットを理解したうえで従わないメリットが大きいと判断した場合はその限りではありません。
PORO を使うから Active Model を使わない
そういう選択もあるでしょう。
しかし Acitve Model のバリデーションなど強力なサポートが受けられないのはつらくなることも多いのでは?
PORO に拘りすぎるのも考え物です。
concern
を使わない
Rails ではおなじみ concern
は使っているでしょうか?
PORO vs concern
の対立構図で語られしまうこともあるアレです。
言いたいことはこちらの記事で解説されているので読んでみると良いかと思います。
PORO を使うから concern
は使わないといった類のものではなく組み合わせて使うことで効果を発揮します。
Active Record を中心に据えてコードを書くならば(いや、中心に据えなくても)コードをコンテキストごとに整理する場合などに効果的です。
劣化版 Active Record にしかならない Repository クラス
Repository クラスが有用なシーンは確かにあります。
しかし多くの場合ではメソッドに分離する程度で問題なかったりします。
Active Record のメソッドをただただラップしたような劣化版 Active Record になってしまっていませんか?
Active Record で簡単に実現できることまでわざわざクラスを作ることがシンプルと言えるでしょうか。
フレームワークなどで定められたやり方以外をしないわけでもない
ここまでの話で Active Model や Acitve Record を使い concern
でコードを整理する Rails が提供しているやり方だけを使うのがシンプルだと言っているように見えるかもしれません。
しかしそうではありません。
標準の方法だけで実装すると、巨大な神 ActiveRecord モデルが爆誕してしまったり、複数のモデルに跨る(どちらが主ともわからない)処理をどこに書くか問題が発生したり、外部 API にアクセスする処理がモデルのプライベートメソッドに実装されていて DB のトランザクション内で呼び出されたり……などなど扱いにくい状態になるときがあります。
適宜ユースケースレイヤーを導入したり、ドメインモデルを PORO などを使って実装したりするほうが見通しが良くなるシーンでは積極的に活用していくのが良いと考えています。
例を挙げるのが難しいところですがあえて挙げるとすると、関連する複数のモデルを辿ったりする必要があるユースケースを実現する場合は「例 4」のような実装よりも「例 5」のようにユースケースクラスを使います。
class Project < ApplicationRecord
def sugoku_fukuzatsu_method(params)
# 関連モデルを辿ったりすごく複雑な処理
end
end
class SugoiContext::SugokuFukuzatsuUseCase
def initialize(project, params)
# パラメーターをインスタンス変数に保持
end
def call
# すごく複雑な処理
end
end
おわりに
シンプルさというのものは言語やフレームワークの特性によっても変わってくるものだと考えています。
特性を理解しうまく使い分けていくのが良いのでは。
シンプルにやろう。そして要はバランスです。
おしまい。
明日の READYFOR Advent Calendar 2024 の 11 日目は TaketoWakabayashi さんによる記事です。お楽しみに!
「みんなの想いを集め、社会を良くするお金の流れをつくる」READYFORのエンジニアブログです。技術情報を中心に様々なテーマで発信していきます。 ( Zenn: zenn.dev/p/readyfor_blog / Hatena: tech.readyfor.jp/ )
Discussion