委譲 (: delegation) とはオブジェクト指向プログラミングにおいて、あるオブジェクトの操作を一部他のオブジェクトに代替させる手法のこと。

概要 編集

委譲を行うオブジェクトは委譲先オブジェクトへの参照を持ち、必要に応じてその参照を切り替える事で動作にバリエーションを持たせる事ができる。一種の実装遅延、プラグイン機構である。一例としては、オブジェクトの編集を行う時、編集の前処理、後処理を本処理と独立させ委譲先に任せる事で、オブジェクト本体の変更を最小限にとどめ局所性を向上させる、などがある。

操作の代替という観点では他に代理 (Proxy) と呼ばれる手法があるが、この場合は代理側のオブジェクトが実体への参照を保持する事で操作のフィルタを行う概念であり、実装の分離を目的とする委譲とは異なる。

委譲を引き受けるオブジェクトはどのような操作を実装しなければならないか知っている必要があるため、インタフェースと併用される場合が多い。

いくつかの言語ではデザインパターンにおける手法であり、いくつかの言語ではコードで実装されライブラリとして提供されている。

編集

C++ 編集

#include <iostream>

struct ExampleInterface {
    virtual void Print() = 0;
};

class Something1 : public ExampleInterface {
public:
    void Print() {
        std::cout << "Something1::Print() is called." << std::endl;
    }
};

class Something2 : public ExampleInterface {
public:
    void Print() {
        std::cout << "Something2::Print() is called." << std::endl;
    }
};

class SomethingDelegator {
    ExampleInterface *implementer;
public:
    SomethingDelegator() : implementer() {
    }

    void setImplementer(ExampleInterface *something) {
        implementer = something;
    }

    void ExecuteProcess() {
        // 処理の詳細をExampleInterface::Print関数の実装者に委譲している
        if (implementer) {
            implementer->Print();
        }
    }
};

int main() {
    SomethingDelegator delegator;

    Something1 source1;
    delegator.setImplementer(&source1);
    // Something1::Print関数が呼ばれる
    delegator.ExecuteProcess();

    Something2 source2;
    delegator.setImplementer(&source2);
    // Something2::Print関数が呼ばれる
    delegator.ExecuteProcess();

    return 0;
}

Java 編集

class A {
    public void foo() {
        System.out.println("A.foo() is called.");
    }
}

class B {
    private A a = new A();
    public void bar() {
        a.foo();
    }
}

JavaScript 編集

SelfJavaScriptといったプロトタイプベースの言語では、言語機能として委譲と継承の中間とも言えるプロトタイプ機能を備えており、委譲先のオブジェクトを書き換えることはできないものの手軽に委譲ができるようになっている。

プロトタイプを使った委譲の例:

/*
共通の反復処理を提供するオブジェクトとしてIterableを定義。

// Iterableはprototype(委譲先)の要件としてdoEachを要求する。
this.doEach = function( aBlock )
{
	// subclassResponsibility;
}
*/
Iterable = function()
{
	this.copyEmpty = function()
	{
		return [];
	}
	
	// 委譲先に追加する要素の選択関数
	this.select = function( aBlock )
	{
		var result = this.copyEmpty();

		this.doEach
		(
			function( anEach )
			{
				if( aBlock( anEach ) ) result.push( anEach );
			}
		);

		return result;
	}
}

BaseCollection = [ 0, 1, 2, 3, 4 ];
BaseCollection.doEach = function( aBlock )
{
	for( var i = 0; i < this.length && false !== aBlock( this[ i ] ); ++i ) ;
}
// 配列にselect( aBlock )を追加する。
Iterable.prototype = BaseCollection;
Collection = new Iterable().select( function( aEach ){ return 3 < aEach } );

BaseCollection = { 0: 4, 1: 3, 2: 2, 3: 1, 4: 0 };
BaseCollection.doEach = function( aBlock )
{
	for( var i in this )
	{
		if( false === aBlock( this[ i ] ) ) return;
	}
}
// 連想配列にselect( aBlock )を追加する。
Iterable.prototype = BaseCollection;
Collection = new Iterable().select( function( aEach ){ return 3 < aEach } );

なお、Selfの様に純粋なプロトタイプベースの言語ではprototype変数やnewに相当するものは無い。単純にプロトタイプとなるオブジェクトを委譲先としてオブジェクトを生成する機能のみ備わっているため、JavaScriptの様に委譲元を再利用するにはメソッド等サブルーチンの追加が必要となる。

Objective-C 編集

Objective-Cの例を説明する。Objective-Cではデリゲート先のオブジェクトはどんなオブジェクトでも構わない。デリゲートしないときはnilを指定してもよい。Objective-CC++Javaとは異なり実行時解決を採用しているので、デリゲート先に指定したオブジェクトが必要なメソッドを持っていなくてもコンパイルエラーにはならず、単に何も実行されないだけである。文法上で特殊な扱いはされておらず、平時の記述の枠内で処理される典型的な実装パターンとして用いられる。

あるオブジェクトAがメインウィンドウを持っていて、そのAにメインウィンドウの処理を委譲する場合

[mainWindow setDelegate: self];

selfは自分自身を指すポインタである。[ ]内はC言語に対してObjective-Cで拡張された文法で、インスタンス変数mainWindowに対してsetDelegateメッセージを送っている。

このdelegateを設定することで、Aがメインウィンドウの処理、たとえばウィンドウを閉じる前に何かしたい場合に用いるメソッドwindowWillCloseをかわりに実行するようになる。Aに

-(void)windowWillClose:(NSNotification*)notification
{
    // 処理
}

のようなメソッドを用意しておけば、ウィンドウのクラス(NSWindow)に修正を加えたり、NSWindowを継承したクラスを作らなくても振る舞いをカスタマイズすることができる。

Smalltalk 編集

Smalltalkにおける委譲も基本的には他の言語と同じである。 ただし、メッセージ機構を利用することでSmalltalk独特の委譲を行うことが出来る。 Smalltalkは、オブジェクトに対しメッセージが送られた際、メッセージ内のセレクターに該当するメソッドがオブジェクト内に無ければ、そのメッセージはdoesNotUnderstand:メソッドに送られる。この時doesNotUnderstand:メソッドで受け取ったメッセージは自由に他のオブジェクトに送りつけることが出来る。

なお、Objective-CにもdoesNotUnderstand:と同等の仕組みが存在し、同様の処理を記述できる。上記のObjective-Cの委譲もこのメッセージ処理の仕組みを利用している。

委譲用クラスの定義例:

"他のオブジェクトに委譲するクラスの登録"
Object
        subclass:               #MouseDelegator     "ObjectクラスからDelegatorクラスを派生させる。"
        instanceVariableNames:  'deletage filter'   "委譲先のフィールド( メンバー変数 )を定義"
        classVariableNames:     ''
        poolDictionaries:       ''
        category:               'ExampleDelegate'.
        
MouseDelegator methodsFor: 'accessing for delegate'
!
deletage
	^deletage
!
deletage: anObject
	deletage := anObject.
!!

MouseDelegator methodsFor: 'accessing for filter'
!
filter
	^ filter.
!
filter: anObject
	filter := anObject.
!!

MouseDelegator methodsFor: 'event handling'
!
click
	"clickメッセージをaTargetに委譲する"
	self filter click.
!!

MouseDelegator methodsFor: 'message fowarding'
!
doesNotUnderstand: aMessage 
	"click以外のメッセージは全てdelegateに委譲する。"
	aMessage sendTo: self delegate.
!!

MouseDelegator class methodsFor: 'instance creation'
!
withDelegate:   aDelegateObject
withFilter:     aFilterObject

	"MouseDelegatorのインスタンスオブジェクトを生成し、初期化する。"
	^ ( self new ) deletage: aDelegateObject; filter: aFilterObject.
!!

委譲用クラスの使用例:

| delegator |

delegator := MouseDelegator withDelegate: ( OtherExample new ) withFilter: ( Example new ).
delegator click.       "Exampleのインスタンスオブジェクトにclickメッセージが送られる。"
delegator doubleClick. "OtherExampleのインスタンスオブジェクトにdoubleClickメッセージが送られる。"

参考 編集

Go 編集

Go言語においては、他の言語と異なり始めから委譲を想定した委譲専用構文を備えている。 Go言語では、表向き継承機能を持っていないが、この委譲構文によって継承に必要とされる殆どの機能を実現できる。例えば多重継承も可能であるが、基底型から派生型のメンバーを呼び出すようなことは出来ない。

下記にGoの委譲機能を使って、線分の頂点座標しか表示できない型に対して、Bezier曲線の頂点座標表示能力を付与する例を示す。

type Point2D struct {
        X, Y float64
}
 
func AxisX( point Point2D ) float64 {
        return point.X
}
 
func AxisY( point Point2D ) float64 {
        return point.Y
}
 
/* 線分描画機能を定義した型 */
type SimpleContext interface {
        Location() Point2D
        MoveTo( point Point2D )
        LineTo( point Point2D )
}
 
/* 線分描画及びBezier曲線描画機能を定義した型 */
type BezierContext interface {
        Location() Point2D
        MoveTo( point Point2D )
        LineTo( point Point2D )
        CubicBezierTo( control1, control2, point Point2D )
}
 
/* SimpleContext型にBezier曲線描画機能を付与する型 */
type BezierAdapterContext struct {
	/*
	委譲先の型名を記述する事でBezierAdapterContextに対し呼び出された
	Location、MoveTo、LineToは、記述した型に自動で委譲される。
	*/
        SimpleContext
}
 
func ( this *BezierAdapterContext ) CubicBezierTo( control1, control2, end Point2D ) {
        start := this.Location()
 
        cubicBezier := func( axis func( Point2D ) float64, t float64 ) float64 {
                dis_t := 1.0 - t
 
                return  axis( start ) * math.Pow( dis_t, 3 ) +
                        axis( end ) * math.Pow( t, 3 ) +
                        3 * t * dis_t *
                        ( axis( control1 ) * dis_t *  + axis( control2 ) * t )
        }
 
        split := 10
        for i := 0; i < split; i++ {
                t := float64( i ) / float64( split )
 
                this.LineTo( Point2D {
                        X:cubicBezier( AxisX, t ),
                        Y:cubicBezier( AxisY, t ),
                } )
        }
}

/* 線分の現在地管理し、入力された線分座標を表示する型 */
type BasicContext struct {
        current Point2D
}
 
func ( this *BasicContext ) Location() Point2D {
        return this.current
}
 
func ( this *BasicContext ) MoveTo( point Point2D ) {
        this.current = point
}
 
func ( this *BasicContext ) LineTo( point Point2D ) {
        this.MoveTo( point )
        fmt.Printf( "%f @ %f\n", point.X, point.Y )
}
 
func main() {
	/*
	BezierAdapterContext型からBasicContext型への委譲関係構築。
	BasicContextに対し、Bezier曲線の座標を表示する能力が付与される。
	*/
        base_context := BasicContext{}
        var context BezierContext = &BezierAdapterContext{ SimpleContext: &base_context }

	/* BasicContext型に委譲される */
        context.MoveTo( Point2D{ X:0, Y:0 } )
        context.LineTo( Point2D{ X:20, Y:20 } )

	/* BezierAdapterContext型が直接処理する */
        context.CubicBezierTo( Point2D{ X:50, Y:-100 }, Point2D{ X:100, Y:100 }, Point2D{ X:200, Y:0 } )
}

Ruby 編集

crubyでは標準添付のdelegateライブラリを利用して委譲が使える。

関連項目 編集