スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
Prev.    Category    Next 

多重継承禁止とインターフェース, Mix-in の許可

多重継承の問題と言語ごとの解決策に関する話を書いてみました。
ただ、 Ruby 好きのため、若干 Ruby よりの記述になっています。

多重継承の問題と Object クラス

多重継承

多重継承で問題になるのはメソッド名などが重複した場合です。
特にダイヤモンド継承(ひし形継承)と呼ばれる共通の祖先を持つクラスの継承では、 必ずメソッド名の重複は起こりますし、どっち経由の祖先のメソッドかわからなくなります。
ダイヤモンド継承
多重継承が許されている言語としては C++ や Python などがあります。
C++ では virtual な継承を使ったり、 Python では書いた順でメソッドの優先度を決めたり といった方法で共通祖先の継承をできるようしています。

しかし、どのメソッドを使うかの問題が解決しても、 共通のメンバー変数(属性)を別々のクラスのメソッドから入り乱れてアクセスすることになり、 不測の問題を引き起こしがちです。

ちなみに JavaScript はプロトタイプベース、 Google が開発した Go 言語では匿名フィールドといった感じで、これらは継承のシステム自体ちょっと違います。 ただ、多重継承っぽいこともできて、やれば似たような問題を引き起こします。

Object クラス

共通の祖先を持たないように多重継承すればいいかというと、 これもやりたくない事情があります。

C# や Ruby といった言語では 全てのクラスが共通の一つの基底クラスから派生しています。 この基底クラスはたいてい Object という名前になっています。
Object クラス
一つのクラスから派生することによって色々なメリットがあります。 その一つとしては ToString() のような全てのクラスが共通して持つメソッドを作れることです。
例えば、 C++ から C# では printf 系での出力位置の指定の仕方が変わりました。
    [C++] printf("%s 。 %d 時です\n", "こんにちは", 12);
    [C#]  Console.Write("{0}。 {1} 時です\n", "こんにちは", 12);
こういったことができるのも、整数など基本型を含めたすべてのクラスが文字列への変換関数を持っているからです。

一つのクラスから継承するメリットは大きいため、 この方式を捨てることは難しいでしょう。 そうなってくると必然的に多重継承はダイアモンド継承になってしまいます。


そこで、"多重継承は問題を起こす、だったら多重継承を禁止しよう" ということになります。
しかし、全面禁止にしたくない理由もあります。 今度はそういった事情を考えてみましょう。

インタフェース

ジェネリックプログラミング

C++ で vector などのコンテナークラスをソートするとします。
 std::vector<MyClass> ary;
 std::sort(ary.begin(), ary.end());
このコンテナークラスに格納するオブジェクトのクラス(MyClass)に < 演算子 が定義されてさえいれば、ソートは可能です。 < 演算子というと C++ 特有感が強いですが、strcmp() のような 比較して数値を返す compare() メソッドが実装されている必要があると考えてください。

C++ のテンプレートのように、型を気にせず、 そのオブジェクトがメソッドをもっているかだけが重要になってくるのが、 ジェネリックプログラミングです。
C++ ではテンプレートを使った時だけジェネリックですが、 Ruby, JavaScript といったスクリプト系の多くは言語全般がジェネリックで、 関数定義などで型指定する必要がありません。

ただし、型指定が悪いわけではありません。 静的な型チェックはコードの安全面で大きなメリットです。
以前 Google が JavaScript の後継として Dart という言語を出してきました。 この Dart では JavaScript の改良として型指定が導入されました。

Go 言語 や PHP では関数の引数定義で型指定するかどうかで、 簡単に型チェック、ジェネリックを切り替えらるようになっています。 こういったスタイルが一番使いやすいのではないかと思います。

メソッド定義の保障

前節の C++ の sort の話で、 compare 関数( < 演算子 )が定義されていない型を渡すとどうなるでしょうか?
これは STL に残っている問題の一つで、かなり意味不明なコンパイルエラーが発生します。
ジェネリックなスクリプト言語の場合は実行時に compare() が呼ばれた時点で エラーとなります。

"sort() に渡す型は compare() が定義されていなければならない" ということが、ジェネリックプログラミングではドキュメントに書かれていて、 それをプログラマーが守って使うことになります。
これは C++ で const 演算子を使わず、 IN だの OUT だのといったコメントだけで、 使い方を示しているようなものです。 もっとコードで明示的に示され、 コンパイル時などでちゃんとチェックしてくれる方がいいでしょう。 これを実現するのが、インターフェースです。

Comparable interface
インターフェースとは振る舞いだけが定義されたクラスです。
引数の型として Comparable を指定すると Comparable を継承したクラス以外を渡した時点でエラーとなり、 関数の入り口でのチェックが可能となります。 また、インターフェースを継承するクラスでは compare() の実装を強制されます。

インターフェースは必要なメソッドが定義されていることの保障として役に立ちます。
それでいて、属性やメソッドの実体を持たないクラスなので、 多重継承しても問題は発生しません。
そのため、 Java, C#, Dart, PHP といった型指定できる言語では、 多重継承は問題が起きるので禁止するけど、インターフェースは許可する というのが主流になってきています。

Mix-in

概念

インターフェース以外にも 多重継承しても問題を起こさず、役に立つものとして Mix-in があります。 Ruby では多重継承は禁止されてますが、 Mix-in は使えるようになっています。

多重継承の例に挙げた鯨の話に戻って考えてみます。 哺乳類と魚を継承して鯨というよりも、 "泳げる" という機能を共に持っているとみなします。こちらの方がより現実に近いのではないでしょうか。 こういったクラスに機能だけを追加することを Mix-in といいます。
Mix-in

インターフェースでは、属性もメソッドの実装も持ちませんでしたが、 Mix-in では、 メソッドの実装はありますが、継承される属性を持ちません。
これにより、多重継承時の問題を回避しています。

Comparable

もっと実際的な話にしましょう。 インターフェースで出てきた compare() ようなキーとなるメソッドがクラスに定義されているとします。 このクラスに Mix-in をすれば キーメソッドから導出できる様々なメソッドが使えるようになります。
例えば、 Ruby では compare() に当たる <=> 演算子を定義して、 Comparable を Mix-in すると ==, <, <= といった比較演算子が全て使えるようになります。
つまり、自分で作ったクラスに対して簡単に比較の機能を追加できるわけです。
Mix-in を使えば、 compare() を実装するというインターフェースと同じ労力で、 より優れたクラスが作れるということです。
[Mix-in]     if ( a < b) {
[Interface]  if ( a.compare(b) < 0) {
演算子のオーバーロード自体に批判的な意見もあります。 しかし、めちゃくちゃに使えばもちろんわかりにくくなりますが、 適切に使えば直感的にも読みやすく、書きやすいクラスを作ることができます。

Enumerable

Ruby でもう一つよく使われる Enumerable を見ていきましょう。 これはコンテナーに機能を追加する Mix-in です。

N 分木のような自作のコンテナークラスを作ったとします。
Enumerable のキー関数は要素一つ一つに順にアクセスする each() というメソッドです。 この each() メソッドを持っていれば、 検索(find, include?, min), フィルター(collect), ソート(sort), 変換(map) といったコンテナークラスで欲しいメソッドが使えるようになります。
つまり、自作のクラスに each() を実装するだけで、 組み込みクラスさながらの便利なクラスにすることができるということです。

ただ、機能的にはインターフェース系の言語でも近いことはできます。 例えば、 find() といった関数を用意して、 そこに each() のようなメソッドが定義されたクラスだけを渡せるようにしておく といった具合です。
しかし、書き方が変わってきます。
[Mix-in]       ntree.find(5)
[Interface]    find(ntree, 5)
インターフェースでは find() の引数にコンテナーを渡すのに対して、 Mix-in ではコンテナーに find() の機能を付加します。
Mix-in ではよりオブジェクト指向的に書けると言えるでしょう。 このため、"純粋"なオブジェクト指向言語である Ruby で採用されいるのだと思います。

まとめ

インターフェースと Mix-in は決して排他的なものではありません。 Mix-in は言語の機能というよりも多重継承の使い方の一つなので、 C++ のように多重継承が許可されていれば、 Mix-in はやろうと思えばできます。

多重継承は問題を起こしがちなので、 禁止しようというのもその通りだと思います。 インターフェースだけ許可し、多重継承は禁止するというのが、 今の言語の主流のような気がします。
しかしながら、インターフェースだけしか許可していないと Mix-in は作れません。 非常に残念な傾向にあると言えるのではないでしょうか。

一方、 Ruby では多重継承を禁止し、かつ Mix-in は使えます。 "やっぱり、 Ruby って最高" と結論付けたいところですが、 Ruby を使っていて、型指定やインターフェースの必要性を感じないというわけでもありません。

私としては多重継承は禁止し、インターフェースと Mix-in だけ許可 というのが理想的なんじゃないかと思います。
実際、最近では Dart はインターフェースに加えて Mix-in が後から追加されましたし、 C# にも LINQ という Minx-in 風の機能が追加されました。 今後はこういった流れが増えてきてほしいと思います。



関連記事
スポンサーサイト
Prev.    Category    Next 

Facebook コメント


コメント

コメントの投稿

Font & Icon
非公開コメント

このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
10位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
2位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

07月 | 2017年08月 | 09月
- - 1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31 - -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。