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