委譲チョットデキル人向けの要約
「委譲」や「デリゲート」が指すものはプログラマーによって違うことがある。
少なくとも下記3パターンあって、あまりにも初見殺しすぎるというお話です。
- Forwarding(転送)のこと。GoFがDelegationであると誤用して広まった。元は誤用なのにこれが多数派。
- JavaScriptなどのプロトタイプベースオブジェクト指向言語などでよく出てくる真のDelegationのこと。Henry Liebermanが定義した。(※JSの場合は暗黙の委譲をよく使っているはず)
- .NET Frameworkの仕様であるDelegatesや、その実装のこと。
転送と委譲の違いについて分かりやすくまとめた画像
想定読者
- 専門書や記事を読んで委譲やデリゲート(delegate/delegation)という単語が出てきて、何それ?と思ってる人
- ある日、天啓があって「プログラミング 委譲」で検索してきた人
- 継承、ポリモーフィズムなどの文法を一通り知っている、C#やJavaなどのクラスベースオブジェクト指向言語のプログラマー
- JavaScriptなどのプロトタイプベースオブジェクト指向言語で継承をやったことがあるプログラマー
- JavaもしくはC#を読むことができ、そしてJavaScriptを読むことができるプログラマー
この記事のゴール
委譲と転送の違いが理解できるようになること。ある文脈で「委譲」や「デリゲート」という文字を目にした時、その言葉が指すのは委譲なのか、転送なのか、それとも全く別の何かなのかを判断できるようになること。
デリゲートとは?
デリゲートというと難しく聞こえますが、実はその「言葉の意味」自体は簡単です。
デリゲートとは委譲のことです。他の何者かに、 委(ゆだ)ね、譲(ゆず)る ことです。
delegateは動詞で「委譲する」
delegationは名詞で「委譲」
課長「A社さんの保守、今日から君に譲るわ。うん、大変なお客様かもしれないが、若いうちは何事も経験さ」
部下「冗談だろ!?(はい。分かりました)」
みたいなことです。
デリゲートはあなたの生活の一部なのです。
簡単じゃん! デリゲート完全に理解したわ!
これで理解できたら苦労しません。
デリゲートはなぜ難しいか
デリゲートの難しさは2つの構成要素からできています。
- 人やコミュニティによって指し示すものが違う
- デリゲートの書き方/文法そのものが複雑
難しさ1:人やコミュニティによって指し示すものが違う
プログラマーのいうデリゲートには、少なくとも私が知っている限りでは、大きく分けて3つの種類があります。
- クラスベース言語のデザインパターンによく出てくる「転送(Forwarding)」の誤用
- 学問的に正しい意味での委譲(Delegation)。プロトタイプベース言語で登場することが多い
- .NET Frameworkで高階関数の実装を実現できる仕様であるDelegatesのこと
なんか難しげな気配が漂ってきましたね。
現時点では、なんかデリゲートが指すものは色々あるらしいなと知っておくだけでよいです。
複数の指し示す対象があるせいで、委譲・Delegation・デリゲートといくら調べても、
自分の求めるデリゲートと、そうでないデリゲートの情報が濁流のように押し寄せて、
わけがわかるものもわけがわからなくなります。
難しさ2:デリゲートそのものが複雑
デリゲートそのものも、複雑で難しく見えます。少なくとも、私が1年目の新入社員だったら、たとえ1つのデリゲートだって理解できなかったことでしょう。
デリゲートを克服するには、ある程度はプログラミングに慣れている必要があります。
クラスのフィールドに別クラスのインスタンスを持たせてそのラッピーのメソッドに処理をぶん投げたり、「クラスをもとに生成されるオブジェクト」とは違う「プロトタイプから作られるオブジェクト」という概念の言語が出てきたり、関数オブジェクトを定義したりします。
「は?」
わけわかんないですよね。でもわけわかってもらいます。
どのデリゲートも、デリゲートを知らなかった人にとっては複雑に思えて、拒絶反応を起こすかもしれません。
けれどもちゃんとプログラマーとしてキャリアを積むのであれば、デリゲートという文字列はよく目にするものになるかもしれません。分からない気持ち悪さを解消するために、共に頑張りましょう。
いくつかの種類のデリゲート
以下では、いくつかのデリゲート(と呼ばれるものたち)について説明していきます。
あまりデリゲートデリゲート言っていると、どのデリゲートについて話しているのか私自身が分からなくなってくるため、これから登場するデリゲート系の用語のここでの使い方を決定しておきます。
下記のルールに則って書いていきます。
ここでの用語の定義
デリゲート
なんかふわっとした言葉。転送/委譲/.NET Delegatesをすべて含めている。
ここで扱うデリゲート的なもの全体を指す。
転送(Forwarding)
その1で紹介する用語。デリゲートのうちの一つ。
実際には委譲と言われることが多いので、たまにカッコをつけて「委譲」とも書く。
委譲(Delegation)
その2で紹介する用語。デリゲートのうちの一つ。真のデリゲート。
.NET Delegates
その3で紹介する用語。.NETの言語仕様。デリゲートのうちの一つ。
おそらく、「デリゲート」で検索するとこれが一番ヒットする。
Javaの人のためにざっくり説明すると、.NETはC#やVBで必須なフレームワークのこと。
delegate(.NET Delegates)
その3で紹介する、.NET Delegatesの実装。C#で予約語となっている。デリゲートのうちの一つ。
System.Delegate型(.NET Delegates)
その3で紹介する、.NET Delegatesの実装。.NETのクラスライブラリのクラス。デリゲートのうちの一つ。
(1)デザインパターンによく出てくる、「転送(Forwarding)」の誤用
誤用?
最もよく知られているデリゲートは、転送(Forwarding)の誤用です。
ただ、元々誤用ではありますが、このデリゲートだけ知っておけば、とりあえずは一息つくことができます。クラスベースのオブジェクト指向言語の専門書でも、このデリゲートだけ知っていれば大丈夫。
けれども、誤用なんだということは理解しておいて損はありません。主なメリットとして、ある日、デリゲート原理主義者に 「オマエの言ってる委譲は委譲ちゃうわ!」 とマサカリを投げられてもびっくりせずに済むというものがあります。
本当は転送(Forwarding)というのが正しいんですが、 むしろ転送という言葉の方がマイナーなので通じない可能性が高いです。
なぜ本当は転送であるべきはずのものが委譲として広まってしまったのでしょうか。
一説によれば、GoF(Gang of Four)のデザインパターン本で「転送」が「委譲」として紹介されてしまったことが原因であると言われています。1
有名なFowlerの『リファクタリング』でも、結城浩の『Java言語で学ぶデザインパターン入門』でも、ついでに2018年10月26日現在の日本語Wikipediaの「委譲」のページでも、転送が委譲として紹介されています。
誤解のないように言っておくと、今先に挙げた2冊の本は、どちゃくそ良い本です。
そういった良書に登場するほどに、転送の誤用としてのデリゲートは広まっているということです。
そもそも、JavaやC#などの言語では本来的な意味での委譲(Delegation)が登場することは とても稀 です。書くとしたら難解になります。本来の委譲(Delegation)は、転送の次に紹介しますが、JavaScriptのようなプロトタイプベースオブジェクト指向言語で言語としてサポートされているため、こちらで登場することが多いです。
転送(Forwarding)の書き方
そういうわけで、まずここでは転送の書き方について説明します。小難しい転送の定義については後回しにしましょう。
転送を制する者はデリゲートを制すと言っても過言ではありません!
クラスベースオブジェクト指向プログラミング言語では、継承という機能が存在します。
転送は、この継承と対になるように登場することがあります。継承と、転送。「この場合は継承より転送の方がいいね」みたいに言います。(※実際には転送ではなく、「委譲」と書かれることがほとんどです。)
転送は、継承という言語としての機能を、継承を使わずに実装する方法として使われることが多いです。継承と転送はよく似ていて、やろうとしていることは基本的に同じです。
それでは実際に継承で書く場合と、転送で書く場合を比較して、どのように実装に違いが出るのか見てみましょう。
ここでは「哺乳類(Mammal)」クラスと、それを継承する「人間(Human)」クラスについて考えてみます。
継承を使って実装する(これは転送ではない)
Mammal
クラスをこんな風に実装したとします。なんてことはありません。
例はC#で書いていますが、見た目はJavaと似たようなものなので、Javaが読めれば読めます。
public class Mammal
{
private readonly string _identity = "Mammal";
public void IntroduceMyself()
{
Console.WriteLine("I am a " + _identity); // 大事なのはメンバ変数を呼んでいるとこ。
}
}
そして呼び出し。
var mammal = new Mammal();
mammal.IntroduceMyself(); // I am a Mammal
うん、いいね。
そして、Mammal
を継承したHuman
を実装します。
人間らしくTalk()
メソッドを追加しましょう。
// 継承版
public class Human : Mammal // HumanはMammalを継承している、という書き方
{
private readonly string _identity = "Human";
public void Talk()
{
Console.WriteLine("A " + _identity + " is talking...ぺちゃくちゃ...");
}
}
うん、良いんじゃないかな。
筆者は「Human
のインスタンスはTanakaやSuzukiみたいな個人じゃないと変だ!」とか、そういうオブジェクト指向べき論について一切気にしない人で、ついでに例もかなり適当です。
さあ呼び出しはどうだ!
// 継承版
var human = new Human();
human.IntroduceMyself(); // I am a Mammal(HumanなのにMammal。これ大事)
human.Talk(); // A Human is talking...ぺちゃくちゃ...
……おや? IntroduceMyself()
で"I am a Human"と言って欲しかったんだけどな……。
と思うかもしれませんが、これは仕方ないことです。継承がしていることは、親クラスを子クラスの内部でnewしているのと同じで、子クラスはそれを使っているだけです。ここではOverrideも何もしていないのでIntroduceMyself()
は親クラスのものそのものですし、参照するのは親クラスの_identityです。"I am a Human"と言わせたければ、Overrideして再定義してやる必要があります。
この継承によるIntroduceMyself()
の挙動は、転送(Forwarding)ではない委譲(Delegation)を理解する上でもとても大事なので覚えておいて下さい。
まぁ"I am a Mammal"「私は哺乳類です」でも間違っちゃいないので、ここでは、子クラスでのOverrideは行わないで完了とします。
転送を使って実装する
さて、継承を使って実装できたので、上で書いたHumanクラスと 全く同じように振る舞う 転送バージョンを書いてみます。
Mammalクラスは前に作ったものをそのまま再利用します。
// 転送版。
// この場合、ForwardingHumanはMammalをラップしているラッパークラスであるという。
public class ForwardingHuman
{
private readonly Mammal _mammal = new Mammal(); // メンバ変数にMammalを持っている!
private readonly string _identity = "Human";
public void IntroduceMyself()
{
_mammal.IntroduceMyself(); // 転送する
}
public void Talk()
{
Console.WriteLine("A " + _identity + " is talking...ぺちゃくちゃ...");
}
}
そして呼び出し。うん、継承と一緒ですね。
相変わらずの"I am a Mammal"
素晴らしい。完成! これこそ転送だ!
// 転送版
var fhuman = new ForwardingHuman();
fhuman.IntroduceMyself(); // I am a Mammal
fhuman.Talk(); // A Human is talking...ぺちゃくちゃ...
// 比較用。継承版
var human = new Human();
human.IntroduceMyself(); // I am a Mammal
human.Talk(); // A Human is talking...ぺちゃくちゃ...
「めんどくさっ」
ちょっと待ってください!!!!!!!!!!
転送は大事です。転送が継承よりも優れた設計となるケースもあります!
残念ながらここでは主題から逸れるので詳細に説明はできませんが、後で少しだけ触れましょう。
まずは、転送の定義について確認しておきましょう。
転送(Forwarding)とは
英語のwikipediaの"Forwarding (object-oriented programming)"のページから引っ張ってきます。
In object-oriented programming, forwarding means that using a member of an object (either a property or a method) results in actually using the corresponding member of a different object: the use is forwarded to another object. Forwarding is used in a number of design patterns, where some members are forwarded to another object, while others are handled by the directly used object. The forwarding object is frequently called a wrapper object, and explicit forwarding members are called wrapper functions.
筆者訳:
オブジェクト指向プログラミングにおける転送(Forwarding)は、あるオブジェクトのメンバ(プロパティかメソッドのどちらか)を使うことが、実際には別のオブジェクトの対応するメンバを呼び出していることを意味します。つまり、別のオブジェクトを「使う」ことが、その別のオブジェクトに対して「転送している」ということです。
転送は多くのデザインパターンで使用されています。そのデザインパターンでは、いくつかのメンバは別オブジェクトに転送する一方で、残りのメンバは直接、オブジェクト内で処理しています。転送する側のオブジェクトはしばしばラッパーオブジェクトと呼ばれ、明示的に転送しているメンバのことはラッパー関数と呼ばれます。
上で私が書いた例でいうと、人間(ForwardingHuman)クラスは哺乳類(Mammal)クラスをラップした(=包み込んだ)、ラッパーオブジェクトなわけですね。ForwardingHuman
クラスのメンバであるIntroduceMyself()
メソッドがMammal
クラスのIntroduceMyself()
メソッドに転送しているということです。
ちなみにラップする側のラッパー(wrapper)に対して、ラップされる側をラッピー(wrappee)ということもあります。ラッパーはラッピーに転送します。
転送がやりたいことは、基本的に継承と同じことです。振る舞いの再利用がしたいだけです。
じゃあなんで継承で書かないの? と思うかもしれません。もちろん理由はあります。継承で書けるなら、継承で書いても良いのです。しかし継承ではどうにもならないケースというのも確かに存在します。たとえば、継承では、子クラスから見た親クラスは常に固定化されています。親クラス1に対して、子クラスがnある関係です。子クラスが1で、親クラスがnにはなれません。継承している子クラスは、別の親クラスに継承先を切り替えることができません。そのため、次で紹介するような転送を使ったデザインパターンが登場します。
転送の応用
ここでは転送の使い所の1例として、Strategyパターンの使い所をほんのちょっと紹介しますが、 本題から逸れるためStrategyの説明自体はしません。Strategyの説明については、別の記事に「委譲」します。 Strategyについてもっと知りたければ、デザインパターンを紹介している技術書や記事などを参照してください。
さて、人間には人種があります。そして個人には性格があります。人間クラスをもっとしっかり設計する上で、次のような特徴をもつものを実装することにしました。
- 人種4つ:コーカソイド・ネグロイド・モンゴロイド・オーストラロイド
- 性格4つ:神経質・外交的・調和的・勤勉
そして、それぞれを実装するインスタンスにこんな振る舞いをさせたくなりました。
// モンゴロイドで外交的な人間。
mongoloidDiplomacyHuman.Invite(); // Let's party!
// コーカソイドで外交的な人間。
caucasoidDiplomacyHuman.Invite(); // Let's party!
// モンゴロイドで勤勉な人間。
mongoloidDiligenceHuman.Invite(); // Let's write a program
mongoloidDiplomacyHuman.TalkAboutRace(); // Skin color of Mongoloid is generally yellow
caucasoidDiplomacyHuman.TalkAboutRace(); // Skin color of Caucasoid is generally white
mongoloidDiligenceHuman.TalkAboutRace(); // Skin color of Mongoloid is generally yellow
// すべてに共通の処理
mongoloidDiplomacyHuman.Laugh(); // Hahahaha
caucasoidDiplomacyHuman.Laugh(); // Hahahaha
mongoloidDiplomacyHuman.Laugh(); // Hahahaha
// 実際の呼び出しではポリモーフィズムを使う。
// 何が呼び出されるかは分からない。
human.Invite();
human.TalkAboutRace();
human.Laugh();
人種と性格の組み合わせは4*4=16通りです。コーカソイド神経質、コーカソイド外交的、コーカソイド調和的、コーカソイド勤勉……オーストラロイド勤勉まで、全部で16通りの組み合わせを作る必要があります。
継承で作った場合は、コーカソイド神経質クラスは、コーカソイドクラスを継承して作るようになるでしょう。そうすると、コーカソイドクラス、ネグロイドクラス、モンゴロイドクラス、オーストラロイドクラスといったスーパークラスがそれぞれ必要になり、4+16の合計20個のクラスが出来上がります。多すぎますね。更に人種や性格は、より細分化するなどして増える可能性があります。最初に一回だけ実装するなら、脳死しながらひたすらクラスを追加していくだけです。ちょっと手間で、地味で退屈ですが、簡単な作業です。簡単さは大切ですよね。
しかし現実は非情であり、いつか機能修正や機能追加の時が訪れます。もしかしたら、案外すぐかもしれません。
「あの、現場からですね……人種と性格はそれぞれ8種じゃないとこのシステムは使い物にならないって言われるんですけど」
無理に継承を使う必要はありません。Humanクラスの中にif文で実装するという手もあります。この場合はそちらの方が簡単でしょう。new Human("mongoloid","diplomacy")
のようにすると、Humanのプロパティとして引数の値がセットされ、IntroduceMyself()の中にそのプロパティを読む条件分岐を沢山作っておくのです。ifの分岐が沢山できますが、これもまた簡単な作業です。
でも、これはただの例だからそれで済むのであって、現実にはこうした分岐の中の処理がConsole.WriteLine("Let's party!")
だけで済むはずがありません。もっと複雑で長い処理になることでしょう。
ifのコードブロックは巨大化し、1ファイルがやがて数千行になってしまうことが想像できます。これではおちおちパーリーもできません。
そうならないためにStrategyが使えます。
……。
もし、Strategyパターンを知らないというのであれば、Strategyについて学び、モンゴロイドで外交的な人間などをぜひ実装してみて下さい。
おそらく2つのinterfaceと4つの人種クラス、4つの性格クラス、そしてそれらに転送をする人間クラスができるはずです。
最近流行り(?)のDI(Dependency Injection)コンテナのもとになっているのはDIパターンで、DIパターンはStrategyパターンそのものです。応用できるところはとても多いので学んでいて損はないはずです。
最後に、転送を使ったデザインパターンの代表例を挙げておきます。
- Chain of Responsibility
- Decorator
- Strategy
他にも色々あります。
(2)Henry Liebermanが定義した、真の委譲(Delegation)
これこそが本当の委譲
お待たせしました。
学術的には、ここで紹介する委譲(Delegation)こそが唯一絶対のデリゲートです。デリゲート原理主義者が崇拝するデリゲート唯一神はこの委譲のことです。
OOPSLA (Object-Oriented Programming, Systems, Languages & Applications) という、2018年現在に至るまで毎年開催されているオブジェクト指向プログラミングの学会があります。
その第1回目である1986年、当時MITの研究者だったHenry Liebermanが"Using Prototypical Objects to Implement Shared Behavior in Object Oriented Systems"(筆者訳:「オブジェクト指向システムで共通の振る舞い(Behavior)を実装するための原型的な(Prototypical)オブジェクトの使用」)という論文を発表しました。
86年当時のものではありませんが、Lieberman自身の手で更新されたその論文は、ここで読むことができます。その論文の中で、Delegationが初めて登場しました。
Henry Liebermanが発案したDelegationは、プロトタイプベースのオブジェクト指向プログラミング言語"Self"で実装されました。そしてそのSelfに大きな影響を受けたのが、現在フロントエンドで大活躍中のJavaScriptです。Delegationの実装は、JavaScriptにしっかりと引き継がれています。プロトタイプチェーンの根っこの考え方に、Delegationそのものがあります。
JavaScriptでは言語として委譲(Delegation)が強力にサポートされており、そのプロトタイプチェーンを用いて勝手に行われる委譲のことを 暗黙の委譲(Implicit Delegation) と言ったりします。
ここでは、一番目にする機会が多いであろう、暗黙の委譲についてご紹介します。
おいおい、そもそもプロトタイプベースのオブジェクト指向言語って何だよ!
主題からちょっと逸れますが、必要なため、プロトタイプベースについて少しだけ説明します。Javaのようなクラスベース言語では、クラス(設計書)とインスタンス(実体)がありますが、JavaScriptのようなプロトタイプベース言語では、そんなものありません。ないんですよオブジェクト指向言語なのに! びっくりしました? ……とは言っても、JavaScriptはES6以降でclassの糖衣構文が書けるので、「ない」と言ってしまうのは乱暴かもしれません。そのclassは内部的にプロトタイプを使って「見かけ上」クラスとインスタンスを実現していますが、本質的にはプロトタイプベースです。「JavaとJavaScriptは似ている」というのは誤りで、根本的な部分が全く異なっています。
プロトタイプベースは、インスタンスベースという別名もあるくらいで、ただインスタンスがあるだけです。クラス(設計図)とインスタンス(実体)ではありません。オブジェクトの全てが 独立した インスタンスです。新しく実体を作るときには、クラスから組み立てるのではなく、既存のインスタンスから新しいインスタンスを作成します。そしてその作成元を、プロトタイプ(原型)と呼びます。なのでプロトタイプベースと呼びます。
(※ちなみにクラスベース言語を対象としたGoFのデザインパターンでも、クラスではなく既存のインスタンスから新しいインスタンスを作成するPrototypeパターンがあります。こっちではクローン(複製)しますが、JSではインスタンス作成時に全く同じものを作るクローンを行うわけではありません)
プロトタイプをもとにして新しくインスタンスを作るときに、クラスベースオブジェクト指向言語に出てくる、継承のようなことがしたくなるわけです。でも、プロトタイプの考え方では、作成元と新規のオブジェクトはそれぞれ独立した別のものです。ん? まるっと同じメンバ変数やメソッドを新しく複製するとしたら、メモリの無駄じゃないの?
そこで登場するのが委譲(Delegation)です。
JavaScriptで委譲(Delegation)を学ぶ
それではJavaScriptのコードを見てみましょう。ここでは「あえて」class構文を使わず、昔ながらのprototypeで書いてみます。実際のプロジェクトでは書かないで下さいね。場合によっては怖い先輩に殺されます。
ここでは、Creature
←Animal
←Mammal
←Human
でオブジェクトが繋がっています。
やっている内容は上のC#の例で紹介した、Mammal
←Human
の例と大差ありません。
// 定義しているのはクラスっぽく見えるけど、クラスではない。
// すべてがインスタンス。
function Creature () {};
Creature.prototype.identity = "Creature";
Creature.prototype.introduceMyself = function(){ console.log("I am " + this.identity) };
Creature.prototype.alive = function(){ console.log(this.identity + " is alive...生存中...") }
// ------------------------------------------
function Animal(){};
Animal.prototype = new Creature(); // これで継承っぽいことが実現できる
Animal.prototype.identity = "Animal";
Animal.prototype.walk = function(){ console.log(this.identity + " is walking...散歩中...") };
// ------------------------------------------
function Mammal(){};
Mammal.prototype = new Animal();
Mammal.prototype.identity = "Mammal";
Mammal.prototype.breastFeed = function(){ console.log(this.identity + " is breastFeeding...授乳中...") };
// ------------------------------------------
// ※さらっと大事なことを言うと、コンストラクタでのthis.~~への代入は、
// 新しく作られた(newされた)オブジェクトそのもののプロパティとして登録される。
function Human(identity){this.identity = identity};
Human.prototype = new Mammal();
Human.prototype.identity = "Human";
Human.prototype.talk = function(){ console.log(this.identity + " is talking...ぺちゃくちゃ...") };
大事なのはここからです。Humanから新しいインスタンス、Tanakaを作りましょう。
(newされた後のhuman
インスタンスは完全にHuman
とは別物なので、前みたくhuman = new Human();
としてしまうと、流石にそれは分かりにくいかな……? と思って今回はTanaka
に変えました。)
// Tanaka はHumanから生成されたObject。
// ※JSでは、newされたあとはもはやHumanではない。Tanakaという別のオブジェクト。
const Tanaka = new Human("Tanaka");
Tanaka.introduceMyself(); // どうなる?
Tanaka.talk(); // どうなる?
Tanaka.walk(); // どうなる?
Tanaka.breastFeed(); // どうなる?
Tanaka.alive(); // どうなる?
Tanaka.introduceMyself();
の実行結果はどうなると思いますか?
もう騙されんぞ!
introduceMyself();
はCreature
で定義されているから、identity:"Creature"
を読む!
答えは"I am Creature"だ! バカめ! 冠詞のaが抜けてるぞ! "I am a Creature"だ!
……そう思いましたか? そう思ったあなたは、上の転送(Forwarding)の内容をしっかり理解している人です。素晴らしい。
でも、今回は結果が違います。
これが暗黙の委譲(Implicit Delegation)です。
ここに、転送と委譲の違いを見ることができます。
I am Tanaka
Tanaka is talking...ぺちゃくちゃ...
Tanaka is walking...散歩中...
Tanaka is breastFeeding...授乳中...
Tanaka is alive...生存中...
どういうことだってばよ。
さて、Tanaka
の中には、Creature
やAnimal
やMammal
はおろか、Human
インスタンスの複製さえも存在しません。Tanaka
が持っているのは、Creature
やAnimal
やMammal
やHuman
インスタンスへの参照です。その参照はどこにあるかというと、Tanaka
オブジェクトの__proto__
という隠しプロパティに存在します。JSの仕様です。例のように、継承される側のインスタンスでprototype
に代入した値は、そのインスタンスをnewした時に、新しく作られる側のインスタンスの__proto__
に放り込まれます。
つまり、const Spider = new Creature();
とすると、Spider.__proto__
にCreature.prototype
が入ります。
そしてAnimal.prototype = new Creature();
とすると、Animal.prototype.__proto__
に、Creature.prototype
が入るということです。クソややこしいですね。
Tanaka
の中にある値は、__proto__
を除くとnew Human("Tanaka");
した時に代入されたidentity:"Tanaka"
だけです。Human
のコンストラクタであるfunction Human(identity){this.identity = identity};
だけが、Tanaka
オブジェクトにプロパティを登録します。
// ちなみに、prototypeではなく、Tanaka自身にメソッドを定義して呼び出すことももちろん可能。
// ただ、prototypeの中に定義しないものは、次のインスタンスをnewしたときに__proto__には入らない。
// つまり継承されないので注意。
Tanaka.learn = function(){ console.log(this.identity + " is learning...プロトタイプわけわからん...") };
Tanaka.learn(); // Tanaka is learning...プロトタイプわけわからん...
図で分かりやすくしましょう。
Tanaka
の中身はどうなっているかというと、こうなっています。
__proto__
の中に__proto__
が入れ子になっていることが分かります。
Chrome Developer Toolsの表示上、Tanaka
の中にCreature
やAnimal
やMammal
やHuman
が存在してそうな雰囲気がありますが、実際はそれらのオブジェクトへの参照が入っています。
さぁ、Tanaka.introduceMyself();
が実行された時、JavaScriptがどう動くかを見てみましょう。
Tanaka
でintroduceMyself()
が呼び出された。そんなメソッドある?→ないね。でもHuman
への参照があるね。
じゃあHuman
にある?→ないね。でもMammal
への参照があるね。
じゃあMammal
にある?→ないね。でもAnimal
への参照があるね。
じゃあAnimal
にある?→ないね。でもCreature
への参照があるね。
じゃあCreature
にある?→あった!!!あったよ!!!introduceMyself()
があった!!!これ呼び出したれ!!!
この繋がりを プロトタイプチェーン といいます。
Creature
のintroduceMyself()
を呼び出すことが決まりました。
しかしTanaka
オブジェクトで勝手にCreature
をクローンして呼び出すことはありません。
ではどうするのかというと、単純です。introduceMyself()
を、 あたかも元々Tanaka
のメソッドであったかのように、素知らぬ顔で呼び出す のです。そのため、introduceMyself()
の中にあるthis.identity
は、元々introduceMyself()
が定義してあったCreature.prototype
にあるidentity
ではなく、Tanaka
のidentity
を使用します。
これが暗黙の委譲です。
つまり、 introduceMyself()
を、Tanaka
のものとして扱う ということです。
そのため、次のコードと全く同じ挙動をします。
const Tanaka = new Human("Tanaka");
Tanaka.introduceMyself = function(){ console.log("I am " + this.identity) }; //
Tanaka.introduceMyself(); // I am Tanaka
以上が、JavaScriptにおける暗黙の委譲の説明です。
じゃあ暗黙じゃない、 明示的な委譲(Explicit Delegation) はどうしたらいいの?
もうお分かりかもしれませんね。__proto__
が空っぽで、Human
と何の関わりもないSuzukiを作ります。
const Suzuki = {};
Suzuki.identity = "Suzuki";
Suzuki.introduceMyself = Creature.prototype.introduceMyself; // これは上で定義したやつ
Suzuki.introduceMyself(); // I am Suzuki
これで完璧。
さて、委譲の定義を確認しましょう。
委譲(Delegation)とは
英語のwikipediaの"Delegation (object-oriented programming)"のページから引っ張ってきます。
In object-oriented programming, delegation refers to evaluating a member (property or method) of one object (the receiver) in the context of another original object (the sender). Delegation can be done explicitly, by passing the sending object to the receiving object, which can be done in any object-oriented language; or implicitly, by the member lookup rules of the language, which requires language support for the feature. Implicit delegation is the fundamental method for behavior reuse in prototype-based programming, corresponding to inheritance in class-based programming.
筆者訳:
オブジェクト指向プログラミングで、委譲は、送信側のオブジェクトの文脈で受信側のオブジェクトのメンバ(プロパティやメソッド)を評価することを指します。送信側のオブジェクトを受信側に渡すことで、どんなオブジェクト指向言語でも、委譲は明示的に行うことができます。あるいは、言語で機能としてサポートされていれば、暗黙的に行うこともできます。暗黙の委譲はプロトタイプベースプログラミングで振る舞いの再利用を行うための基本的な方法で、クラスベースプログラミングにおける継承に相当するような存在です。
さぁ、言ってる内容が分かりましたでしょうか。上で挙げた例では、Tanaka
が送信側のオブジェクトで、Creature
が受信側のオブジェクトです。そしてCreature
のintroduceMyself()
が、Tanaka
の文脈で評価されているということです。それから、暗黙の委譲も大丈夫ですね。でも、まだ一つ分からないことがあります。 「送信側のオブジェクトを受信側に渡すことで、どんなオブジェクト指向言語でも、委譲は明示的に行うことができます。」 と書かれていますね。つまり、C#やJavaでも委譲ができると書いてあります。それをどうするかは先に回しましょう。
転送と委譲の決定的な違い
さて、転送と委譲の決定的な違いを図でまとめました。この内容が理解できたら、あなたは転送・委譲マスターです。
このスライドではJavaScriptはモダンなclassの書き方を使っています。コンストラクタ内でsuper();
が強制されること以外は、やってる内容に違いはありません。
委譲と転送の違いはこれで完璧ですね!
クラスベース言語で委譲
C#やJavaでも真の委譲がしたい!という方のために、最後にC#での委譲の書き方だけ説明しておきます。ただ、言語として暗黙的にサポートされているJavaScriptの委譲よりも書き方として難解になるため、どうしても、そう、 どうしても 必要に迫られない限りは避けた方が良いでしょう。
先に挙げたwikipediaの 「送信側のオブジェクトを受信側に渡すことで、どんなオブジェクト指向言語でも、委譲は明示的に行うことができます。」 というのは、このような書き方のことを指しています。
サクッと書きましょうね~。
// identityを取得するためにinterfaceを定義する。
// 名前の先頭に"I"をつけるのはinterfaceでよくある命名。
public interface IIdentity
{
string GetIdentity();
}
// 受信側。IIdentityを実装してるよ。
public class DelegatingMammal : IIdentity
{
private readonly string _identity = "Mammal";
public string GetIdentity()
{
return _identity;
}
// interface型の引数なのがポイント。
// あと、デフォルト引数でnullにして、nullだった場合にthisをセットする。
// 引数が(IIdentity iidentity = this)だとC#の都合でコンパイルが通らない。
public void IntroduceMyself(IIdentity iidentity = null)
{
if (iidentity == null)
{
iidentity = this;
}
Console.WriteLine("I am a " + iidentity.GetIdentity());
}
}
// 送信側。こっちもIIdentityを実装してるよ。
public class DelegatingHuman : IIdentity
{
public readonly string _identity = "Human";
private readonly DelegatingMammal _mammal = new DelegatingMammal();
public string GetIdentity()
{
return _identity;
}
public void IntroduceMyself()
{
_mammal.IntroduceMyself(this); // 委譲する。thisを突っ込むのがポイント。
}
public void Talk()
{
Console.WriteLine(_identity + " is talking...ぺちゃくちゃ...");
}
}
そして実行。
var dmammal = new DelegatingMammal();
dmammal.IntroduceMyself(); // I am a Mammal
var dhuman = new DelegatingHuman();
dhuman.IntroduceMyself(); // I am a Human
今回、送信側の文脈として評価しているのは、_identity
ではなくGetIdentity()
です。送信側のオブジェクトをそのままinterfaceであるIIdentity
型の引数として渡すことで、送信側のGetIdentity()
を呼んでいるわけです。
これでオッケー。しっかりと"I am a Human"ですね。
(3).NET Frameworkの仕様である.NET Delegates
またまたそんな、ご冗談を
さぁ、第3のデリゲートは.NET Frameworkの仕様です。フレームワークの仕様と聞いて、きっと、上で紹介された委譲か転送かを特定の言語で書きやすくしたものだと、あなたは思うことでしょう。違います。
このデリゲートは委譲(Delegation)とも転送(Forwarding)とも一切関係ありません。正しく、「第3の」デリゲートです。
おっと、観客席から激しいブーイングが上がっております!
C#の文脈以外ではこのデリゲートは滅多に登場しないため、C#やVBを書く方以外は、このデリゲートについてあまり気にする必要はありません。
ここでは他のデリゲートと区別するために.NET Delegates
と記していますが、実際に登場する時は単に「デリゲート(Delegates)」と呼ばれることが多いです。
.NETで高階関数
.NET Delegates
という仕様は、.NETで 部分的な関数型プログラミング をするための仕様である……と考えると、おそらく最初は理解しやすいでしょう。.NET Delegates
の実装を使うことで、関数を引数にしたり、関数を返り値にしたりするような関数を定義することができるようになります。そのような関数を 高階関数 といいます。その高階関数が引数にしたり、返り値にしたりする関数の型はすべて、C#では「System.Delegate
型の子クラス」です。System.Delegate
型は.NET Delegates
の仕様をプログラムに落とし込んだものというわけです。
(※【重要】.NET Delegates
によって実現できることは高階関数を書くことだけではありません。これはあくまでそう使われることが多く、そのように考えると、とりあえず.NET Delegates
の使い方の基本を理解できるだろうと思っての説明です)
そしてC#には、delegate
という予約語があります。このdelegate
は「System.Delegate
型の子クラス」を定義するためのものです。どんな風に書くかを例に書いてみましょう。
// 引数(string name) & 返り値なし(void)の関数の型として、
// [System.Delegate型の子クラス]であるDelegateSubIntroduce型を定義するよという意味
// 名前にSubとつけたのはSystem.Delegate型のサブクラス(=子クラス)だから
public delegate void DelegateSubIntroduce(string name);
これ自体はメソッドではありません。「System.Delegate
型の子クラス」、つまりは型です。なので引数にしたり、返り値にしたりすることができます。
さて、LisaクラスのTalk()
メソッドの引数に、さっき定義した関数型を入れてみましょう。例によって超適当な例ですね。なんなんだLisaクラスって。もし関数型プログラミングをこの記事で初めて知ったという人が読んでいたら、本当に申し訳ありません。高階関数の使い所などについて説明するのはこの記事の目的ではないので、もっと適切な例を挙げている別の記事を読んで学んでください。こんな例では使い方は分かっても、いつ使ったらいいかさっぱり分かりません。
public static class Lisa
{
// さっき定義したDelegateSubIntroduceを引数に。これで、関数を引数に持つことができるようになった。
public static void Talk(DelegateIntroduce delIntro, string name)
{
delIntro(name); // ここで引数の関数が実行される。後ろにカッコをつけないと、関数は実行されない。
Console.Write("Hi. I'm Lisa.");
return;
}
}
そして、Talkメソッドの引数として突っ込む予定の、何の面白みもないメソッドを作っておきましょう。
public static class Someone
{
// このメソッドを、メソッドごと変数に突っ込む予定。(※正確にはメソッドへの参照を渡す)
public static void IntroduceMyself(string name)
{
Console.WriteLine("Hi. I am " + name + ".");
}
}
実際に動かします。
// ここでは何も出力されない。メソッドをDelegateIntroduceに代入する(※メソッドへの参照を渡す)だけ。
DelegateSubIntroduce delIntro = Someone.IntroduceMyself;
// ここで初めてコンソール出力される。
Lisa.Talk(delIntro, "Yuki");
出力はこうなります。私が中学生の時、英語の教科書で最初に目にしたのがこの文でした。
Hi. I am Yuki.
Hi. I'm Lisa.
delegate
と、それが作り出すSystem.Delegate
の子クラスについての説明は以上です。
代表的なSystem.Delegateの子クラス
いちいちDelegateSubIntroduce
型なんて定義したくない、面倒くさい、という人のために、最初から作られている便利なSystem.Delegate
型の子クラスがあります。昔のC#ではこれらはありませんでしたが、今では予約語のdelegate
を使って子クラスを定義することは稀かもしれません。代わりに使うのがこの2つです。
-
Func
(返り値があるとき) -
Action
(返り値がないとき)
他にもありますが、使わなくても済むため、使う機会はそう多くないかもしれません。Func<int,string,bool>
のように書くと、その変数は引数(int, string)、返り値boolの関数を入れられるようになります。これらの使い方については、説明している記事は他に沢山あるため、別の記事にデリゲートすることにしましょう。
おわりに
記事を書いてる途中で、デリゲートデリゲート書きすぎたせいで何回か発狂しかけました。
この記事が、本当に、本当に、本当にややこしいデリゲートへの、あなたの理解の一助となれば幸いです。
今回初めてQiitaの記事を書きましたが、書く上で分からないことが噴出し、色々調べて、私自身めっちゃめちゃ勉強になりました。
長い記事でしたがここまで読んで頂いて、本当にありがとうございました。
内容におかしな点があったら遠慮なくマサカリください。
.NETの仕様についておかしかったところを指摘して下さったsoiさん、ありがとうございます。
重要な更新履歴
- 2018/11/01 3番目のデリゲート、.NET Delegatesの内容についてご指摘を受けたため修正・加筆を行いました。それに合わせて、表題や大項目も
delegate syntax
→.NET Delegates
に変更しました。最初はこのデリゲートはオマケみたいに書いていましたが、より一層、デリケートにデリゲートを扱うようになりました。
参考URL
Wikipedia - Delegation (object-oriented_programming)
Wikipedia - Forwarding (object-oriented_programming)
Jim Gay - The Gang of Four is wrong and you don't understand delegation
Henry Lieberman - Using Prototypical Objects to Implement Shared Behavior in Object Oriented Systems
microsoft - デリゲートの概要
-
参考:Jim Gay - The Gang of Four is wrong and you don't understand delegation ↩