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

Rubyのデカップリング:委任と依存性注入

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

たとえば、財務レポートを実行するための単純なクラスを作成する場合:

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

FinanceReportと言えます 依存 FinanceApi 、外部の支払い処理業者から情報を引き出すために使用します。

しかし、ある時点で別のAPIをヒットしたい場合はどうでしょうか。または、外部リソースにアクセスせずにこのクラスをテストしたい場合はどうでしょうか。最も一般的な答えは、依存性注入を使用することです。

依存性注入では、FinanceApiを明示的に参照しません FinanceReport内 。代わりに、引数として渡します。 注入

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

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

これで、クラスはFinanceApiが オブジェクトも存在します! 任意のオブジェクトを渡すことができます gross_incomeを実装している限り およびtotal_costs

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

  • コードがFinanceApiに「結合」されなくなりました 。
  • FinanceApiの使用を余儀なくされています パブリックインターフェース経由。
  • テストでモックまたはスタブオブジェクトを渡すことができるようになったため、実際のAPIをヒットする必要がありません。

ほとんどの開発者は依存性注入を検討しています 一般的には良いことです(私も!)。ただし、すべての手法と同様に、トレードオフがあります。

コードが少し不透明になりました。 FinanceApiを明示的に使用した場合 、私たちの価値観がどこから来ているのかは明らかでした。依存性注入を組み込んだコードでは、それほど明確ではありません。

それ以外の場合、呼び出しがselfに送信された場合 、次にコードをより冗長にしました。オブジェクト指向の「オブジェクトにメッセージを送信して動作させる」パラダイムを使用する代わりに、より機能的な「入力->出力」パラダイムに移行していることに気付きます。

これが最後のケースです(selfに送信されたはずの呼び出しをリダイレクトします )今日見たいと思います。これらの状況で依存性注入の可能な代替案を提示したいと思います:基本クラスを動的に変更する (ちょっと)

解決すべき問題

少し前に戻って、私がこの道を歩むきっかけとなった問題、PDFレポートから始めましょう。

私のクライアントは、さまざまな印刷可能なPDFレポートを生成する機能を要求しました。1つのレポートにはアカウントのすべての費用がリストされ、別のレポートには収益がリストされ、別のレポートには将来の利益が予測されます。

由緒あるprawnを使用しています これらのPDFを作成するためのgem。各レポートはPrawn::Documentからサブクラス化された独自のRubyオブジェクトです。 。

このようなもの:

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

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

ここまでは順調ですね。しかし、ここに問題があります。クライアントは、他のすべてのレポートの一部を含む「概要」レポートを望んでいます。 。

ソリューション1:依存性注入

前述のように、この種の問題に対する一般的な解決策の1つは、依存性注入を使用するようにコードをリファクタリングすることです。つまり、これらすべてのレポートでselfのメソッドを呼び出すのではなく 、代わりに、PDFドキュメントを引数として渡します。

これにより、次のようなものが得られます:

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

これは機能しますが、ここにはいくらかのオーバーヘッドがあります。一つには、すべての描画方法でpdfを使用する必要があります。 引数、およびprawnへのすべての呼び出し 今、このpdfを通過する必要があります 引数。

依存性注入にはいくつかの利点があります。それは、システム内の分離されたコンポーネントに向かって私たちを押し進め、ユニットテストを容易にするためにモックまたはスタブを渡すことを可能にします。

ただし、このシナリオでは、これらのメリットのメリットを享受していません。私たちはすでに強く prawnに結合 APIなので、別のPDFライブラリに変更するには、ほぼ確実にコード全体を書き直す必要があります。

ここでもテストは大きな問題ではありません。私たちの場合、自動テストで生成されたPDFレポートをテストするのは面倒で、価値がないからです。

したがって、依存性注入は、必要な動作を提供するだけでなく、最小限のメリットで追加のオーバーヘッドをもたらします。別のオプションを見てみましょう。

ソリューション2:委任

Rubyの標準ライブラリはSimpleDelegatorを提供します デコレータパターンを実装する簡単な方法として。オブジェクトをコンストラクターに渡すと、デリゲーターへのメソッド呼び出しはすべてオブジェクトに転送されます。

SimpleDelegatorの使用 、prawnをラップアラウンドする基本レポートクラスを作成できます 。

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

次に、このクラスから継承するようにレポートを更新できます。レポートは、初期化子で作成されたデフォルトのドキュメントを使用して、以前と同じように機能します。 概要でこれを使用すると、魔法が起こります レポート:

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_table およびCostReport#costs_pie_chart 変更はありませんが、prawnへの呼び出し (例:text(...)move_down 20 など)は現在、OverviewReportに転送されています SimpleDelegator経由 作成しました。

動作に関しては、基本的にSalesReportのように作成しました OverviewReportのサブクラスになりました 。私たちの場合、これはprawnへのすべての呼び出しを意味します のAPIは、SalesReport -> OverviewReport -> Prawn::Documentに移動します。 。

SimpleDelegatorの仕組み

SimpleDelegatorの方法 内部で機能するのは、基本的にRubyのmethod_missingを使用することです。 メソッド呼び出しを別のオブジェクトに転送する機能。

したがって、SimpleDelegator (またはそのサブクラス)はメソッド呼び出しを受け取ります。それがそのメソッドを実装しているなら、素晴らしいです。他のオブジェクトと同じように実行されます。 ただし 、そのメソッドが定義されていない場合は、method_missingにヒットします。 。 method_missing 次に、callを試みます コンストラクターに渡されたオブジェクトのそのメソッド。

簡単な例:

require 'simple_delegator'
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!"

SimpleDelegatorをサブクラス化する 独自のThingDecorator ここのクラスでは、いくつかのメソッドを上書きして、他のメソッドをデフォルトのThingにフォールスルーさせることができます オブジェクト。

上記の簡単な例では、実際にはSimpleDelegatorは実行されません。 しかし、正義。このコードを見て、「Thingをサブクラス化しないでください」とよく言われるかもしれません。 同じ結果が得られますか?」

はい、そうです。ただし、主な違いは次のとおりです。SimpleDelegator 委任先のオブジェクトをコンストラクターの引数として受け取ります。これは、実行時にさまざまなオブジェクトを渡すことができることを意味します 。

これにより、呼び出しをprawnにリダイレクトできます。 上記のソリューション2のオブジェクト。単一のレポートを呼び出す場合、prawn 呼び出しは、コンストラクターで作成された新しいドキュメントに移動します。ただし、概要レポートではこれを変更して、prawnを呼び出すことができます。 そのに転送されます ドキュメント。

結論

依存性注入は、おそらくほとんどに対する最良の解決策です。 デカップリングの問題ほとんど

ただし、すべての手法と同様に、トレードオフがあります。私の場合、DIによってもたらされるオーバーヘッドは、それが提供するメリットに見合う価値があるとは思わなかったので、別の解決策を探しました。

Rubyの他のすべてのものと同様に、常に別の方法があります 。このソリューションにたどり着くことはあまりありませんが、このような状況では、Rubyツールベルトに追加することは確かに素晴らしいことです。


  1. Rubyでのラムダの使用

    ブロックはRubyの非常に重要な部分であり、ブロックなしで言語を想像するのは難しいです。しかし、ラムダ?ラムダが好きなのは誰ですか?あなたはそれを使わずに何年も行くことができます。まるで過ぎ去った時代の遺物のようです。 ...しかし、それは完全に真実ではありません。ラムダを少し調べてみると、ラムダにはいくつかの興味深いトリックがあります。 この記事では、ラムダの使用法の基本から始めて、さらに興味深い高度な使用法に移ります。したがって、ラムダを毎日使用していて、それらについてすべて知っている場合は、下にスクロールするだけです。 Lambdasについて覚えておくべき主なことは、それらが関数の

  2. Ruby2.6の9つの新機能

    Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,