Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで肥大化するUserクラスを防ぐ――ジャンクドロークラス問題の回避策

Rubyはオブジェクト指向言語なので、世界をオブジェクトの集合としてモデル化しがちです。たとえば、2つの整数(xとy)をPoint(点)と呼び、Line(線)はその2つを持つ、という具合です。

このアプローチは多くの場面で有効ですが、大きな問題を1つ抱えています。それは、データに対する特定の解釈を他のすべてより優先してしまうことです。xとyは常にPointであり、Cell(セル)やVector(ベクトル)として振る舞うことはない、と決めつけてしまうのです。

では、Cellが必要になったらどうなるでしょうか。データの所有権はPointにあるため、PointにCell用のメソッドを追加することになります。これを繰り返すうちに、関連性の薄いメソッドが雑多に詰め込まれた、まるで「ジャンクドロー(ガラクタ箱)」のようなPointクラスができあがります。

こうしたジャンクドロークラスはあまりにも一般的なので、「避けられないもの」として受け入れられ、開発工程の最後に「リファクタリング」を付け足すのが当たり前になっています。しかし、そもそも問題を回避することはできないのでしょうか。

Web開発の第一の掟――Userクラスの話はしない

長年の実戦をくぐり抜けてきたRailsアプリケーションの中心には、Userという名の巨大な怪物クラスが鎮座しています。

はじまりはいつも無邪気なものです。ユーザーにログイン機能を持たせたい。ユーザー名とパスワードを保存する必要があるから、クラスを作りましょう。

class User
  attr_accessor :username, :password, :email, :address

  def authenticate!(password)
    ...
  end
end

うまく動いたので、今度は人々がお金を払いたくなりました。「Subscriber(購読者)は基本的にUserと同じものだから、属性とメソッドを少し足せばいいよね」と私たちは考えます。

class User
    ...

    attr_accessor :payment_processor_token, :subscription_plan_id, ...etc

    def charge_cc
      ...
    end
end

最高です!ついに本当の売上が立ち始めました!するとCEOが、営業チームがSalesForceに簡単に取り込めるよう、ユーザーの連絡先情報をVCard形式でエクスポートできるようにしたいと言い出します。さあ、vimを起動しましょう。

class User
    ...

    def export_vcard
      ...
    end
end

私たちは何をしてしまったのか

当初の目的は認証だけだったUserクラス。そこへメソッドと属性を追加し続けた結果、User/Subscriber/Contactが融合したフランケンシュタインのようなクラスになってしまいました。

なぜこんなことをするのでしょうか。開発者としての自分たちを尊重していないのでしょうか。それとも、「このデータは本当にひとつのモノ(オブジェクト)だ」と信じ込んだことで、最初から失敗への道を選んでいたのでしょうか。

なぜ「ユーザー名+パスワード+メールアドレス」の組み合わせをUserと呼ぶのでしょうか。SubscriberでもContactでもSessionHolderでもよかったはずです。

データの正体は、ただのデータ

データはただのデータです。今日は恋人として扱うかもしれませんが、明日には元恋人になっているかもしれません。

Elixirのような関数型言語が取っているのは、まさにこのアプローチです。データはRubyのハッシュや配列、文字列のようなシンプルな構造に格納され、何かをしたいときはデータを関数に渡します。結果はその関数から返されます。

シンプルに聞こえますが、このアプローチにより、関心事を異なるモジュールへきれいに分離することが非常に容易になります。

UserシステムをElixirで構築するイメージは次のようになります。

my_user = %{username: "foo", password: ..., phone: ..., payment_token: ...}
my_user = Authentication.authenticate(my_user)
my_user = Subscription.charge(my_user)
my_user = Contact.export_vcard(my_user)

データがコードから分離されているため、どのモジュールもそのデータについて特権的な解釈を持ちません。

Rubyに持ち帰る

このアプローチがElixirでこれほど効果的なら、Rubyでも採用してはどうでしょう。Userを単なるデータのラッパーにし、ビジネスロジックをすべてモジュールへ追い出せばいいのです。それを妨げるものは何もありません。

class User
  attr_accessor :username, :password, :email, :address
end

module Authentication
  def self.authenticate!(user)
    ..
  end
end

module Subscription
  def self.charge(user)
    ..
  end
end

module Contact
  def self.export_vcard(user)
    ..
  end
end

UserクラスをStructに置き換えたり、コードを追加して(ほぼ)不変(イミュータブル)なデータオブジェクトにしたりすることも可能です。

だから何?

些細な変更に見えるかもしれません。「何の意味があるのか」と思うかもしれません。

しかし、これまで見てきたように、典型的なオブジェクト指向アプローチは時間が経つほど問題を抱えます。特定のクラスがデータを所有すると決めた瞬間、そのデータを別の用途で使いたくなったときに問題が発生するのです。

一方、モジュール方式なら、新しい振る舞いの追加は何の問題ももたらしません。データを必要な形で解釈する新しいモジュールを作ればよいだけです。データと機能が完全に分離しているため、それは簡単に実現できます。

その他のアプローチ

ジャンクドロー問題を防ぐ方法は他にもあります。伝統的なオブジェクト指向プログラミングでは、継承と慎重なリファクタリングによって対処してきました。2012年にはSandi Metz氏が『Practical Object Oriented Design in Ruby』を出版し、多くのRubyistが依存性注入(DI)を使ったオブジェクトのコンポジションを取り入れるきっかけとなりました。さらに近年では、関数型プログラミングの人気を受けて、不変な「データオブジェクト」を実験するRubyistも増えています。

これらのアプローチはいずれも、きれいで美しいコードを生み出せます。しかし、クラスが通常データを所有するという事実により、クラスが何であるかと、クラスがそのデータで何をするかの間には、常に緊張関係が存在します。

こうしたアプローチがジャンクドロー問題の回避に成功しているのだとしたら、その成功は、データをコードから切り離していることに由来するのではないか――私はそう考えています。

  1. 【Ruby入門】開発者が押さえておきたい主要データ構造の特徴と使い方

    データ構造とは? データ構造とは、データを整理し、効率よくアクセスするための具体的な方法のことです。 代表的な例としては以下のようなものがあります。 配列(Array) 二分木(Binary Tree) ハッシュ(Hash) データ構造ごとに得意な処理は異なります。たとえば、ハッシュは辞書(単語と意味)や電話帳(名前と電話番号)のように「キーと値」のペアを扱うデータの保存に最適です。 どのようなデータ構造が存在するのか、そしてそれぞれの特性を理解することは、Ruby開発者としてのレベルアップに直結します。 本記事では、その知識をわかりやすく解説していきます! 配列(Array)を理解する

  2. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端