vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】処理の委譲で迷った話

はじめに

この記事は C# Advent Calendar 2017 の一日目の記事です。

qiita.com

「継承より委譲を」という言葉を、Java開発者の方々を中心に(と思う)よく目にします。

  • クラスの継承をすると、親クラスの変更に子クラスが大きな影響を受けるので変更がしづらくなる
  • 委譲の場合、外部から呼び出しできるメソッド・そうでないメソッドの区別をつけやすい
  • 一般的には子クラスが親クラスの特別な種類である場合(is-a関係)に継承し、
    親クラスが子クラスを含んでいる(例えば子クラスが持つ機能を親クラスが持っている状態。 has-a関係)場合は委譲を用いる

といったところがその理由のようです。

OK。じゃあ処理を委譲しましょう。

と思ったのですが、どのように設計すれば委譲したことになるの?というところで迷ったので、
その辺りをまとめます。

delegateについて

C# 委譲」といったキーワードで検索すると、 delegate についての記事が見つかります。

delegate はごく簡単にまとめるとメソッドを引数として渡せるものです。

// 引数、戻り値が同じメソッドを変数として扱うことができる.
public delegate void CallNoArgMethod();

public class MainController
{
    public MainController()
    {
        var noArgMethod1 = new CallNoArgMethod(NoArgMethod);
        var noArgMethod2 = new CallNoArgMethod(StaticNoArgMethod);

        // NoArgMethod()が実行される.
        noArgMethod1();
        // StaticNoArgMethod()が実行される.
        noArgMethod2();

        // delegateメソッドをひとまとめにすることもできる.
        var mixedMethod = noArgMethod1;
        mixedMethod += noArgMethod2;

        // NoArgMethod()とStaticNoArgMethod()が実行される.
        mixedMethod();

        // delegateを自分で定義せずに Action 、Func (戻り値あり)を使うこともできる.
        var action = new Action(NoArgMethod);
        // 戻り値の型を指定する必要がある.
        var funcWuLongTea = new Func(CallHasReturnValueMethod);
    }
    // delegateを作るメソッド
    public void NoArgMethod()
    {
        Debug.WriteLine("世界さん、ちーっす");
    }
    // staticメソッドでも同じように扱うことができる.
    public static void StaticNoArgMethod()
    {
        Debug.WriteLine("staticだよ");
    }
    public string CallHasReturnValueMethod()
    {
        return "Hello";
    }
}

一般的には処理が完了したあと呼び出し元に通知するためのコールバック、ボタン押下などのイベント、
Linqなどで用いられる無名関数(通常ラムダ式が使われますが)などで使用されます。

var nums = new List {1, 2, 3, 4, 5};
// delegateを使って書く.
var evenDelegate = nums.Select(delegate(int num) { return num % 2 == 0; });
// ラムダ式を使って書く.
var evenLambda = nums.Select(num => num % 2 == 0);

で、これを使ってどうやって委譲するんです?というか、処理を委譲するのにdelegateを使う必要ってあるんです?

はい。ここで迷いました...orz

前述の通り、委譲というのは(例えば)そのクラスが持つ機能を別のクラスに分割する(委譲する)ことらしい。

ただ、これって別に delegate を使う必要はなくて、
処理を別クラスに分けて、メソッドを呼び出してあげれば良いのでは......?

と思ったのですが、あまりに自明すぎるのか、誰も気にしていないのか、この辺りに言及する資料は見つけられませんでした
(日本語で検索した限りでは)。

元の言葉を調べてみよう

ところで、Java界隈で継承と委譲の話が良く出てくる理由に、 Effective Java があるようです。

では、英語でも検索するにあたって、英語版の Effective Java ではどう表現されているかを見てみることにしました。

「Favor composition over inheritance」

ふむふむ。「inheritance」が「継承」なので、委譲にあたる言葉は「composition」のはずですね。

「composition」の意味を調べると「組み立て、合成」など(compositionの意味 - 英和辞典 Weblio辞書)と出てきます。

ほうほう。

......。

「継承より委譲を」の時の「委譲」って「委譲」じゃないんじゃないですか???

......少々取り乱しました。

結局、やるべきことは一つのクラスに対象物が持っている性質や機能をひとまとめにするのではなく、
複数クラスに切り分けて親となるクラスから呼び出すようにしましょう、ということで、
その方法についてはメソッド呼び出しでも delegate でも良い、という理解で良さそうです。

delegateで遊んでみる

せっかくなのでここからは delegate を使ってもう少し遊んでみることにします。

コールバック

何かの処理を行った後、それが完了したことを呼び出し元に伝えてほしい場合があります。
delegateを使うことで、呼び出された側が呼び出し元のクラスインスタンスを持たなくても通知できるようになります。

public void StartControlling()
{
    ExecuteSomeOperationWithCallback(() =>
    {
        // 完了後の処理.
    });
}
private void ExecuteSomeOperationWithCallback(Action callback)
{
    // 何か処理して終わったらdelegateメソッド呼び出し..
    callback();
}

呼び出せる処理を限定する

delegate でメソッドを変数として扱うには、クラスのインスタンスが必要になります。
ただ、クラスのインスタンス自身は、delegate メソッドの変数(メンバー変数として作成)を生成するときにローカル変数として生成しても、
その後で delegate メソッドがGCで空になる、といったことは無いようです。

そのため、呼び出し元のクラスで呼び出すメソッドを限定することができます。

public class MainController
{
    private CallStringArgMethod callMethod;
    public MainController()
    {
        var delegateMethodManager = new DelegateMethodManager();
        callMethod = new CallStringArgMethod(delegateMethodManager.PrintMessage);
    }
    public void StartControlling()
    {
        // このクラスで呼べるのはPrintMessageだけ.
        callMethod("Hello");
    }
}
public delegate void CallStringArgMethod(string message);
public class DelegateMethodManager
{
    public void NoArgMethod()
    {
        Debug.WriteLine("Hello No Arg");
    }
    public void PrintMessage(string message)
    {
        Debug.WriteLine(message);
    }
}

その他

例えばUnityで3Dモデルを動かす、という処理を複数クラスで共通化させた場合。
そのままの名前で呼び出すのではなく、 moveCube 、 carModel など対象物に特化させた名前を付けると、
意図がよりわかりやすくなるかもしれません。

また、メソッド自体は private にしておきたいが、特定の条件でのみ操作したい、といった場合、
intなどの変数でやるように Getter / Setter を使うことができます。

おわりに

一般的に浸透している(と思う)言葉でも、語源に近い言語で調べてみるの大事ですね(苦笑)。

まぁそれはそれとして、 delegate もコールバック関数やLinqで使う以外にも、
使い方によってはなかなか便利そうです。

もちろん乱用は厳禁ですが(;'∀')

明日は neuecc さんです。よろしくお願いいたします(..)_

参照