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

Rubyにおけるデカップリング手法:委譲と依存性注入

オブジェクト指向プログラミングでは、あるオブジェクトが機能するために、別のオブジェクトに依存することはよくあります。

例えば、財務レポートを生成するためのシンプルなクラスを作ってみましょう。

class FinanceReport
  def net_income
    FinanceApi.gross_income - FinanceApi.total_costs
  end
end

この場合、FinanceReportFinanceApi依存していると言えます。外部の決済プロセッサから情報を取得するために、FinanceApiを利用しているわけです。

しかし、将来別のAPIに乗り換えたくなったらどうでしょうか?あるいは、もっと現実的な話として、外部リソースにアクセスせずにこのクラスをテストしたい場合は?最も一般的な答えは依存性注入(Dependency Injection)です。

依存性注入では、FinanceReportの中でFinanceApiを直接参照しません。代わりに引数として渡します。つまり、注入(インジェクト)するのです。

依存性注入を適用すると、クラスは次のようになります。

class FinanceReport
  def net_income(financials)
    financials.gross_income - financials.total_costs
  end
end

これで、このクラスはFinanceApiというオブジェクトの存在すら知りません。gross_incometotal_costsを実装していれば、どのようなオブジェクトでも渡せるようになります。

これには多くの利点があります。

  • FinanceApiとの「結合度」が下がる。
  • FinanceApiを公開インターフェース経由で使うことが強制される。
  • テスト時にモックやスタブを渡せるため、実際のAPIを叩く必要がなくなる。

ほとんどの開発者は、依存性注入は概ね良い手法だと考えています(筆者もその一人です)。しかし、あらゆる技術と同様にトレードオフが存在します。

まず、コードがやや不透明になります。FinanceApiを明示的に使っていた頃は、値がどこから来ているのかが一目瞭然でした。依存性注入を取り入れたコードでは、そこが読み取りにくくなります。

また、本来selfに送られるはずだった呼び出しまで置き換えると、コードは冗長になりがちです。「オブジェクトにメッセージを送り、処理を任せる」というオブジェクト指向のパラダイムから、「入力→出力」という関数型寄りのパラダイムへとシフトしてしまうのです。

本記事で注目するのは、まさにこの最後のケース——self宛ての呼び出しのリダイレクト——です。こうした場面で依存性注入の代替となる手法として、基底クラスを動的に差し替える(ような)アプローチを紹介します。

解決すべき課題

少し立ち戻って、筆者がこの道を辿ることになった発端の問題から始めましょう。それはPDFレポートの生成です。

クライアントから、さまざまな印刷用PDFレポートを生成できるようにしてほしいという要望がありました。あるレポートはアカウントの全経費を一覧表示し、別のレポートは売上を一覧表示し、さらに別のレポートは今後数年の予想利益を示す——といった具合です。

実装には老舗のprawn gemを採用し、各レポートはPrawn::Documentを継承した独立したRubyオブジェクトとして構築しました。

イメージとしては次のようなコードです。

class CostReport < Prawn::Document
  def initialize(...)
    ...
  end

  def render
    text "Cost Report"
    move_down 20
    ...
  end
end

ここまでは順調です。ところが、ここで問題が発生します。クライアントは、他のすべてのレポートの内容を部分的に含んだ「Overview(概要)」レポートを求めてきたのです。

解決策1:依存性注入

前述のとおり、この種の問題に対する定番の解決策は、依存性注入を使うようにリファクタリングすることです。つまり、各レポートがselfのメソッドを呼び出す代わりに、PDFドキュメントを引数として渡す方式へ変更します。

コードは次のようになります。

class CostReport < Prawn::Document
...
  def title(pdf = self)
    pdf.text "Cost Report"
    pdf.move_down 20
    ...
  end
end

これは確かに動作しますが、相応のオーバーヘッドが伴います。まず、すべての描画メソッドがpdf引数を受け取る必要があり、prawnへのすべての呼び出しがこの引数を経由することになります。

依存性注入自体には利点があります。システム内のコンポーネントの疎結合化を促進し、モックやスタブを注入できるためユニットテストが容易になるのです。

しかし残念ながら、このシナリオではその恩恵をほとんど受けられません。私たちはすでにprawn APIと強固に結合しており、別のPDFライブラリへ乗り換えるなら、ほぼ間違いなくコード全体の書き直しが必要になるでしょう。

テスト面の心配も小さかったです。このケースでは、生成されたPDFレポートを自動テストで検証するのはあまりに煩雑で、割に合わないと判断していたからです。

つまり依存性注入は、望みの動作は実現してくれるものの、メリットが少ない割にオーバーヘッドだけが増えてしまうのです。そこで、別の選択肢を見てみましょう。

解決策2:委譲

Rubyの標準ライブラリには、デコレータパターンを手軽に実装できるSimpleDelegatorが用意されています。コンストラクタにオブジェクトを渡すと、デリゲータへのメソッド呼び出しがそのオブジェクトへ転送されます。

SimpleDelegatorを活用すれば、prawnをラップする基底レポートクラスを作れます。

class PrawnWrapper < SimpleDelegator
  def initialize(document: nil)
    document ||= Prawn::Document.new(...)
    super(document)
  end
end

続いて、各レポートをこのクラスの継承に変更します。イニシャライザで生成されるデフォルトのドキュメントを使う限り、従来どおり動作します。魔法が起こるのは、この仕組みをOverviewレポートで使うときです。

class OverviewReport < PrawnWrapper
  ...
  def render
    sales = SaleReport.new(..., document: self)
    sales.sales_table
    costs = CostReport.new(..., document: self)
    costs.costs_pie_chart
    ...
  end
end

ここで注目すべきは、SaleReport#sales_tableCostReport#costs_pie_chartのコードは一切変更されていない点です。それにもかかわらず、text(...)move_down 20といったprawnへの呼び出しが、作成したSimpleDelegatorを経由してOverviewReportへ転送されるようになっています。

動作の観点で言えば、あたかもSalesReportOverviewReportのサブクラスであるかのように振る舞わせているわけです。このケースでは、prawnのAPIへのすべての呼び出しが、SalesReport -> OverviewReport -> Prawn::Documentという流れで処理されます。

SimpleDelegatorの仕組み

SimpleDelegatorが内部でどのように動いているのかというと、基本的にはRubyのmethod_missingを活用して、メソッド呼び出しを別のオブジェクトへ転送しています。

SimpleDelegator(またはそのサブクラス)がメソッド呼び出しを受け取ると、そのメソッドを自分で実装していれば、通常のオブジェクトと同様に実行します。しかし、該当メソッドが定義されていなければmethod_missingに到達し、コンストラクタに渡されたオブジェクトに対してそのメソッドの呼び出しを試みます。

簡単な例で見てみましょう。

require 'delegate'

class Thing
  def one
    'one'
  end

  def two
    'two'
  end
end

class ThingDecorator < SimpleDelegator
  def two
    'three!'
  end
end

ThingDecorator.new(Thing.new).one #=> "one"
ThingDecorator.new(Thing.new).two #=> "three!"

独自のThingDecoratorクラスでSimpleDelegatorを継承することで、一部のメソッドはオーバーライドしつつ、残りのメソッドは元のThingオブジェクトへフォールスルーさせています。

ただし、この程度の例ではSimpleDelegatorの真価は伝わりません。「Thingを普通にサブクラス化しても同じ結果になるのでは?」と思った方もいるでしょう。

その通り、同じ結果になります。しかし重要な違いが一つあります。SimpleDelegatorは、委譲先のオブジェクトをコンストラクタの引数として受け取る点です。これはつまり、実行時に任意のオブジェクトを渡せるということです。

まさにこの特性のおかげで、解決策2ではprawnオブジェクト宛ての呼び出しをリダイレクトできています。単体のレポートを呼び出す際は、prawnへの呼び出しはコンストラクタ内で新規作成されたドキュメントへ向かいます。一方、Overviewレポートでは、prawnへの呼び出しが自分自身のドキュメントへ転送されるよう差し替えているのです。

まとめ

依存性注入は、ほとんどのデカップリング問題に対して、たいていの場合最良の解決策でしょう。

とはいえ、あらゆる技術と同様にトレードオフは存在します。筆者のケースでは、DIがもたらすオーバーヘッドは得られるメリットに見合わないと判断し、別の解決策を模索しました。

Rubyの世界では、何事にも必ず別のやり方があるものです。この手法を頻繁に使う機会はないかもしれませんが、こうした状況に備えた便利な道具として、あなたのRubyツールボックスに加えておく価値は十分にあるはずです。

  1. Rubyのラムダ徹底解説――基本から実践的な応用テクニックまで

    Rubyにおいてブロックは非常に重要な存在であり、ブロックなしにこの言語を語ることはできません。では、ラムダはどうでしょうか?ラムダを愛している人はいるのでしょうか?実際のところ、何年もラムダを使わずに過ごすことだって可能かもしれません。まるで昔の時代の遺物のように見えることもあるでしょう。 しかし、それは完全な真実ではありません。少し掘り下げてみると、ラムダには興味深い技が数多く隠されているのです。 この記事では、まずラムダの基本的な使い方から始め、その後、より高度で面白い応用へと進んでいきます。すでに毎日ラムダを使いこなしている方は、ぜひ後半の応用編までスクロールしてください。 ラムダにつ

  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)のように終端