ソフトウェアエンジニアリングにおいて、委譲パターンはオブジェクト指向設計パターンであり、オブジェクトの構成によって継承と同様のコード再利用を実現できるようにするものです。
委譲では、オブジェクトは、別のオブジェクト(デリゲート)に委譲することで要求を処理します。デリゲートはヘルパーオブジェクトですが、元のコンテキストを持ちます。委譲の言語レベルのサポートでは、selfデリゲート内のがデリゲート(受信オブジェクト)ではなく、元の(送信)オブジェクトを参照するようにすることで、暗黙的にこれが行われます。デリゲートパターンでは、代わりに、メソッドの引数として元のオブジェクトをデリゲートに明示的に渡すことでこれが実現されます。[ 1 ] 「委譲」は、送信オブジェクトが受信オブジェクトのコンテキストで評価された受信オブジェクトの対応するメンバーを単に使用する転送という別の概念を指すために、しばしば緩やかに使用されます。
この記事では、2つのオブジェクトに対して「送信オブジェクト/受信オブジェクト」という表現を用い、「受信オブジェクト/デリゲート」という表現は用いず、元の呼び出しではなく、委譲呼び出しを送信および受信するオブジェクトを強調しています。
Gamma et al. 1994 の序論では、委任は次のように定義されている。
委譲は、継承と同じくらい強力な再利用性を実現する合成方法です [Lie86, JZ91]。委譲では、リクエストの処理に2 つのオブジェクトが関与します。受信オブジェクトが操作をデリゲート
thisに委譲します。これは、サブクラスがリクエストを親クラスに委譲するのと似ています。しかし、継承では、継承された操作は、 C++ および Smalltalk のメンバ変数を介して常に受信オブジェクトを参照できますself。委譲で同じ効果を実現するには、受信側が自身をデリゲートに渡して、委譲された操作が受信側を参照できるようにします。[ 2 ]
以下の例(Kotlinプログラミング言語を使用)では、Windowクラスは呼び出しを内部のRectangleオブジェクト(デリゲート)に委譲します。area()
class Rectangle ( val width : Int , val height : Int ) { fun area () = width * height }class Window ( val bounds : Rectangle ) { // 委譲fun area () = bounds . area () }一部の言語には、委譲のための特別なサポートが組み込まれています。たとえば、Kotlinプログラミング言語では、byキーワード[ 3 ]は別のオブジェクトのインターフェースに委譲します。
interface ClosedShape { fun area (): Int }class Rectangle ( val width : Int , val height : Int ) : ClosedShape { override fun area () = width * height }// Window の ClosedShape 実装は、境界となる Rectangle の実装に委譲しますclass Window ( private val bounds : Rectangle ) : ClosedShape by boundsより具体的な例を挙げると次のようになります。
interface Repository { fun save ( data : String ) fun load ( id : String ): String fun delete ( id : String ) }class FileRepository : Repository { override fun save ( data : String ) = println ( "データ$ dataを保存しています" ) override fun load ( id : String ): String = "データ: $ id " override fun delete ( id : String ) = println ( "データ$ idを削除しています" ) }class LoggingRepository ( private val delegate : Repository ) : Repository by delegate { override fun save ( data : String ) { println ( "保存しようとしています" ) delegate . save ( data ) } }この例では、LoggingRepository は Repository と同じインターフェースを実装しつつ、別の Repository インスタンスをラップしています。save メソッドはログ記録を追加するように特化されていますが、その他のメソッドは自動的にデリゲートオブジェクトに転送されます。
これにより、定型的なコードを回避し、FileRepository のような特定の具体的な実装に縛られることなく、リポジトリの動作を拡張することが可能になります。これに対し、継承に基づくソリューションでは、特定の実装をサブクラス化する必要があります。
interface Repository { fun save ( data : String ) fun load ( id : String ): String fun delete ( id : String ) }open class FileRepository : Repository { override fun save ( data : String ) = println ( "データ$を保存しています" ) override fun load ( id : String ): String = "データ: $ id " override fun delete ( id : String ) = println ( "データ$ idを削除しています" ) }class LoggingRepository : FileRepository () { override fun save ( data : String ) { println ( "保存しようとしています" ) super . save ( data ) } }このアプローチでは LoggingRepository が FileRepository に結合されますが、インターフェース委譲では同じラッパーを任意の `Repository` 実装で使用できます。
Kotlinでは、インターフェース委譲はコンパイラによって生成される転送メソッドを通じて実装されます。例えば、前述のWindowクラスは、次のようなコードと同等です。
interface ClosedShape { fun area (): Int }class Rectangle ( val width : Int , val height : Int ) : ClosedShape { override fun area () = width * height }// Window の ClosedShape 実装は、境界となる Rectangle の実装に処理を委譲します。class Window ( private val bounds : Rectangle ) : ClosedShape { override fun area () = bounds . area () }インターフェース委譲と手動オーバーライドを組み合わせると、明示的にオーバーライドされていないメンバーのみが自動的に転送されます。例:
interface Greeter { fun greet () fun greetTwice () }class DefaultGreeter : Greeter { override fun greet () { println ( "hello" ) }override fun greetTwice () { greet () greet () } }class LoudGreeter ( private val delegate : Greeter ) : Greeter by delegate { override fun greet () { println ( "HELLO" ) } }fun main () { val g = LoudGreeter ( DefaultGreeter ())g.greet ( ) g.greetTwice ( ) }このプログラムは印刷します
こんにちは こんにちは こんにちは 結果から、greet は LoudGreeter の明示的なオーバーライドによって処理されるのに対し、greetTwice は DefaultGreeter に転送されることがわかります。DefaultGreeter.greetTwice の内部では、レシーバーはデリゲートオブジェクト自体のままなので、greet() の内部呼び出しは LoudGreeter.greet() ではなく DefaultGreeter.greet() に解決されます。
このため、Kotlin の by 機能は委譲と表現されることが多いが、実際には、元の受信コンテキストが保持されるより強力な委譲形式よりも、コンパイラが生成する転送に近い。[ 4 ]