🌟

シンプルにやろう

2024/12/11に公開

はじめに

言いたいことはこれだけなのですが少し解説していきます。

  • シンプルにやることとシンプルなクラスを作ることはイコールではない
  • フレームワークや言語の流儀に無理に逆らうのは茨の道
  • フレームワークなどで定められたやり方( Rails way, etc... )以外をしないわけでもない

解説は Ruby と Rails を使ったコードになります。
言語やバージョンに強く依存するものではないですが Ruby 3.3 と Rails 8.0 くらいを想定してください。

シンプルにやることとシンプルなクラスを作ることはイコールではない

以下のどちらの例がシンプルに感じますか?

例 1
class Foo1
  include ActiveModel::Validations

  attr_accessor :num

  validates :num, numericality: { only_integer: true, in: 1..100 }
end
例 2
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 クラスのみを使う方式が見通しが良い場合もあるかもしれません。

例 3
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 の対立構図で語られしまうこともあるアレです。

言いたいことはこちらの記事で解説されているので読んでみると良いかと思います。
https://techracho.bpsinc.jp/hachi8833/2023_04_04/127023

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」のようにユースケースクラスを使います。

例 4
class Project < ApplicationRecord
  def sugoku_fukuzatsu_method(params)
    # 関連モデルを辿ったりすごく複雑な処理
  end
end
例 5
class SugoiContext::SugokuFukuzatsuUseCase
  def initialize(project, params)
    # パラメーターをインスタンス変数に保持
  end

  def call
    # すごく複雑な処理
  end
end

おわりに

シンプルさというのものは言語やフレームワークの特性によっても変わってくるものだと考えています。
特性を理解しうまく使い分けていくのが良いのでは。

シンプルにやろう。そして要はバランスです。

おしまい。


明日の READYFOR Advent Calendar 2024 の 11 日目は TaketoWakabayashi さんによる記事です。お楽しみに!

READYFORテックブログ

Discussion