※9/16 タイトルを変更しました。
こんにちは。開発部社員のjiweenです。
前回 は本連載の概要とモチベーション、前提知識について解説しました。今回は本連載の結論として、オブジェクト指向を実践する上で重要な3つのルールを提案します。今回提案するルールが実際にどう応用されるのか、既存のパターンをどう説明できるのかは第3回以降で説明します。
今回の記事は抽象的な記述で言語機能を否定する分かりづらい部分があります。抽象的な記述に慣れない方は、今後公開される第3回以降も併せて読むと分かりやすいかと思います。
- 第1回 はじめに, 概要
- 第2回 結論
- 第3回 Adapter, State, Strategy, Abstract Factory
- 第4回 Template Method, Factory Method, Bridge, Proxy, Composite, Interpreter, Decorator
- 第5回 Visitor, Observer, Iterator, Facade, Mediator
- 第6回 Builder, Singleton, Prototype, Flyweight, Chain of Responsibility, Command, Memento, まとめ
目次(第2回)
- 目次(第2回)
- 結論
- 「流動性を判別できる場合、単一の流動的要素を非流動的要素によってカプセル化する」
- 「流動的要素のカプセル化には関連とインターフェイスを積極的に使う」
- 「データと振る舞いの分離は禁止されない」
- オブジェクト指向に必要な言語機能
- 次回予告
- 採用情報
結論
私が特に重要だと思うオブジェクト指向設計のルールは以下の3つです。
- 流動性(variability)を判別できる場合、単一の流動的要素を非流動的要素によってカプセル化する
- 流動的要素のカプセル化には関連とインターフェイスを積極的に使う
- データと振る舞いの分離は禁止されない
今回の記事ではこれらのルールの意味を少しずつ説明していきます。
各ルールはGoFパターンの説明が合理的に為されるよう試行錯誤した結果得られたものです。GoFパターンとの対応は第3回以降で見ていきます。
補足: 関連、集約、コンポジションの用語的な使い分けについて
クラス図 (UML準拠) ではオブジェクト間の関係として 依存 (dependency) 、関連 (association) 、集約 (aggregation) 、コンポジション (composition) が定義されており、いくつかの用語は設計を語る上でよく使われます。ざっくり言えば依存は一時的な通信 (≒メソッド呼び出し) のような弱い関係であり、関連、集約、コンポジションはいずれも所有関係 (has-aの関係、一方のオブジェクトがもう一方のオブジェクトをとして保持している関係) を指します。集約とコンポジションは関連の特殊な場合とみなされ、使い分けられる場合は関連→集約→コンポジションの順に繋がりが強くなっていきます。しかしどれもhas-aの関係を満たすことに変わりはなく設計者の意図程度の違いしかないので、大抵関連のみが使用されます。本連載でも「依存」と「関連」のみ用います。ただし一般的な日本語としての「依存」を使うことが多いのでご注意ください。
関連には多重度が書かれることがあります。また双方向の関連はただの実線になります。
GoF本を参照する場合、今では非標準となった用語と表記が使用されているので注意して下さい。compositionは関連、集約、コンポジションをまとめて指す場合の「関連」のような意味で用いられ、UML標準における関連 (association) を指す用語として acquaintance が使われています。表記においては、単なる矢印が acquaintance (=associtaion) を表し、矢印の元にひし形を加えることで aggregation (集約) を表しています。UML標準におけるコンポジション (composition) は登場しません。
「流動性を判別できる場合、単一の流動的要素を非流動的要素によってカプセル化する」
流動的要素に関する1つ目のルールは最も重要です。残りのルールもこのルールに依存しています。
オブジェクト指向はそもそも変更 (一種の流動性) に対処するために発展してきたという点から見ても、流動性とその取り扱いを最優先で定めるのは合理的と言えるでしょう。
変更に対応することの重要性は「オブジェクト指向のこころ」でも指摘されています。
多くのバグは、ソースコードの変更によって生み出されている。
あなたにも覚えがあるかもしれません。ソースコードを変更しなければならないものの、他の部分を壊してしまいそうで、気が進まなかったという経験はないでしょうか?何故こういったことが起こるのでしょうか?ソースコードは、自分が呼び出す機能すべてとその使用方法に注意を払わなければならないのでしょうか?関数は他の関数とどのようにやり取りを行うのでしょうか?関数が実装しようとしているロジック、関数がやり取りしている相手とのあれこれ、関数が使用しているデータ等、関数が注意を払うべき詳細が多すぎるのでしょうか?人間と同様、あまりにも多くのものごとを同時に行っているようなプログラムは、ちょっとした変更によっておかしくなってしまうのです。プログラミングは、複雑かつ抽象的でダイナミックな作業なのです。
どれだけ頑張ったとしても、どれだけうまく分析したとしても、ユーザからすべての要件を引き出すなんてできっこありません。明日のことなんて誰にも判らず、万物は流転していくのです。これは絶対不変の真理なのです。
― 「オブジェクト指向のこころ 第2版」 1.2 オブジェクト指向パラダイム以前:機能分解
こういった「システムの変化を特定部分に封じ込めることで、そこ以外の既存部分を再利用する」というメリットが、1990年代中頃になってクローズアップされるようになってきました。そのきっかけともなった書籍が、『オブジェクト指向における再利用のためのデザインパターン』(原著はDesign Patterns: Elements of Reusable Object-Oriented Software)です。
― 「オブジェクト指向のこころ 第2版」訳者まえがき
このルールに登場する「流動」という言葉は「変更」「変化」と似ていますが、微妙な意味合いがあります。以下では、このルールに登場する言葉の意味を少しずつ解説していきます。特に「流動」「カプセル化」は本連載の重要なキーワードになっています。
"流動的要素を非流動的要素によってカプセル化する"
「流動 (英語: vary)」や「流動的要素 (英語: variable)」という日本語訳は「オブジェクト指向のこころ」の和訳本で使用されており気に入ったのでそこから取っています。和訳本で単に「変化」「変わる」といった言葉を使わず「流動」というあまり一般的でない言葉を使っているのは「様々・次々に変わる」というvaryのニュアンスを含ませたかったからだと思われます。 - 同書では「可変性」という似た表現が出てきますが、もとの英語は「variability」なのでより統一的に訳すなら「流動可能性」「流動性」などになります。本連載では流動という言葉を一貫して使うために「流動性」と呼びます。
流動的要素について、「オブジェクト指向のこころ」では次のように説明されています。
Design Patterns: Elements of Reusable Objetct-Oriented Software において GoF は以下のように述べています。
あなたの設計において、何を流動的要素とするべきかを考察してください。
このアプローチは、再設計の原因に着目するというものとは正反対となっています。設計変更を強いる可能性のあるものが何かを考えるのではなく、再設計せずに何を変更可能にするのかを考えるのです。ここで着目しているのは、流動的概念のカプセル化であり、多くのデザインパターンのテーマともなっているものです。1ここで重要な部分を言い直すと「流動的要素を見つけ出し、それをカプセル化する」ということになるのです。
― 「オブジェクト指向のこころ 第2版」 8.4 流動的要素を見つけ出し、それをカプセル化する
- ここでは二重引用と翻訳が絡まっていてややこしいのですが、variable (狭義に "変数" を指すわけではない) という表現をもともと使っていたのはGoF本の原著です。その部分が「オブジェクト指向のこころ」の原著で引用されており、上の文はそれらをまとめて「オブジェクト指向のこころ」の和訳者が翻訳したものです。GoFの和訳本ではvariableは「流動的要素」ではなく「変動要素」と訳されていました。
- GoF本では流動性(variability)についてあまり多くを語っていませんが、「オブジェクト指向のこころ」では流動性に着目し、流動性分析による設計を掘り下げています。
流動という言葉のニュアンスは概ね分かっていただけたかと思います。しかし「オブジェクト指向のこころ」でもGoF本でも、流動性についてあまり厳密な定義は行われていません。
流動性をより正確に理解するための鍵は、GoF本の記述にある「設計変更を強いる可能性のあるものが何かを考えるのではなく、再設計せずに何を変更可能にするのかを考える」という部分だと思います。もちろん設計変更を強いる可能性のあるものが分かっている場合は設計に反映すべきだと思いますが、ここで述べられているのはより積極的に変更可能性を生み出そうということだと思います。つまり、流動性とは変更可能性そのものではなく「変更容易であるような状態」のことであり、「流動的要素」とは「変更容易な要素」です。
私の解釈では、変更可能性が流動的要素にそのまま対応しない理由は次のようなものです。
- 変更可能性が高くても流動的でない要素もある。変更そのものは不確定な未来の可能性であり、全てが設計に反映されるものではない。流動的要素は変更可能性よりも、人間が「変更容易な状態であるべき」だと判断した設計要素に対応する。
- 変更の可能性があるからと言ってそれを流動的要素とみなして完璧に対処しようとする必要はありません。重要性の高いものを優先したり、そうでないものを無視する余地があります。
- 変更可能性が低くても流動的な要素もある。変更可能性が低くても、流動的な要素として設計したほうが楽な場合もある。また、変更箇所を予測するのは難しいため、代わりに備えとしての流動性が必要なこともある。
- 例えば、綺麗な花の種類がきっちり100種類あってそれが永久に変わることはないとしましょう。花が100種類あることは変わらないので、変更可能性だけを気にするのであればルールは適用されません。その場合、最も原始的な方法は100種類の花それぞれを設計要素とみなすことです。しかし、その結果100回も似たようなコーディングを繰り返さなければならないとしたらどうでしょうか?このような巨大な繰り返しは流動的な要素として扱った方が楽に実装できます。2
- また、100個もの繰り返し要素があったとしてそれが全く変更されないということは現実には少ないはずです。花が100種類もあれば、ある日101種類や200種類に増やしたり、どこかの花を仕様変更したくなったりする可能性は全体としては大きくなります。プログラムのあらゆる場所でこのような可能性を考慮するのは大変なので、常に最低限の流動性を意識しておくということは有用です。
このように流動的要素は変更可能性そのものではないため、設計者が判断する必要があります。GoF本で述べられているように、 設計に困ったらまずは何を流動的要素とみなすかを考えるべきです。
相対的に流動的な要素が分かったらそれを切り出し、相対的に非流動的な要素によってカプセル化します。ここで登場するのが「カプセル化」という用語です。
通常カプセル化はデータ (フィールド) の隠蔽のことだと説明されますが、 データの隠蔽だけを指しているわけではなくあらゆる「隠蔽」を指しています。
カプセル化(encapsulation): データ隠蔽として定義されることが多いものの、あらゆるもの(型、実装、設計等)の隠蔽を指していると捉えるべきです。
― 「オブジェクト指向のこころ 第2版」 1.6 オブジェクト指向パラダイム
Design Patterns: Elements of Reusable Objetct-Oriented Software において GoF は以下のように述べています。
(中略、既に引用した部分と同一)
カプセル化という言葉がデータの隠蔽のみを指していると考えた場合、上の文章は意味不明になるはずです。カプセル化が型の隠蔽、すなわち抽象クラスやインターフェイスを用いたクラスの隠蔽も表しているという考え方に立って初めて、この文章の深い意味が理解できるのです。
― 「オブジェクト指向のこころ 第2版」 8.4 流動的要素を見つけ出し、それをカプセル化する
これは私の考えですが、上記のような見方は更に押し進めると多相性 (ポリモーフィズム) はまさに型カプセル化によって生まれる流動性に対応すると思います。値の型の種類を流動的要素とみなし、それをインターフェイスというより非流動的な型によって隠蔽しているからです。つまり型の多相性はカプセル化と直交する概念ではなくカプセル化というより強力な概念を実装するための手段という位置づけになります。3
非流動的-流動的な関係の典型例は多相性、つまり「インターフェイスとクラス」です4が、細かい目で見れば「クラスのメソッド/コンスタクタ宣言とその実装」や、「クラスとそのインスタンス」なども同様の関係を持ちます。 よく言われる「クラス自体がカプセルである」という解釈は狭義のもので、これは2つ目の例、つまり「クラスが外部に見せるメソッドの宣言」と「その実装である手続き処理・データ」がカプセルと中身の関係になっていることを指しています。
"流動性が判別できる場合"
流動的要素・非流動的要素の切り出しは重要ですが、流動性が判別できない場合に無闇に切り出しを行わないというのも重要です。オブジェクト指向設計は変更を織り込み済みなので、リファクタリングを繰り返すような柔軟な開発が可能です。変更が遅すぎなければクラスの機能を切り出すことはそれほど困難ではないので、不確定要素が多いうちは機能の分配を大雑把に行っておき、後で流動性を導入したくなったときにコストを払って適切な単位に分割すればよいのです。構造的なものづくりではトップレベルの視点や大雑把な視点を優先的に使うべきです。
"単一の"流動的要素
既に別物とみなしているはずの2つの流動的要素が混在していると、一方の変更がもう一方の要素に思いがけず波及してしまいます。これではそれぞれの変更に上手く対処することができません。流動的要素が分かっているならそれを一つずつに分けるべきであり、このため "単一の" という強調を加えています。
補足: SOLID原則の「単一責任」
このことは、オブジェクト指向設計の原則として伝統があるSOLID原則でも強調されています。SOLID原則の第一項「単一責任」です。
クラスは変更する理由を1つだけ持つべきである
「単一責任」は要約される場合微妙に表現の異なる様々な言い方がされ、特に名前に使われている「責務(responsibility)」という言葉を用いて説明されることが多いです。個人的な見解では、責任や責務(responsibility)といった言葉選びは誤解を招くと思います。クラスやメソッドの責務=やるべき仕事は単に小さくすべきという解釈ができてしまうからです。注目すべきは流動性の方で、流動性を含まない要素は必ずしもミニマムにする必要はありません。 変更がほとんど起こらないプログラムを高度に抽象化してもメリットはありません。楽しいこと以外には。
SOLID原則の提唱者は、責務と言った場合にイメージされる「その要素のやるべきこと、その要素の表面的な働き」に言及しているわけではなく、正確には単一の流動的要素=変更可能要素に近いものを単一の責務と言っているようです。
wikipediaの「SOLID」のページ (https://en.wikipedia.org/wiki/Single-responsibility_principle) では冒頭で以下のように説明されています
Robert C. Martin, the originator of the term, expresses the principle as, "A class should have only one reason to change," although, because of confusion around the word "reason" he also stated "This principle is about people.".
(この言葉の発案者である Robert C. Martin は、この原則を「クラスが変わる理由(reason)は一つであるべきだ」 と表現しているが、「理由(reason)」という言葉の混乱から「この原則は人に関するものだ」とも述べている。)
しかし責務という言い方が誤解を招くことは間違いないので、歴史を修正できるのなら例えば "単一責任(single responsibility)" ではなく "単一流動/単一変性(single variability)" などと言うべきだったのではないかと思います。
流動性を判別できる場合、単一の流動的要素を非流動的要素によってカプセル化する」
これでこのルールの意味が分かったと思います。特に本連載を読む上で意識して欲しいのは、「流動的要素」が「変更される要素」とは限らないこと、「カプセル化」が「あらゆる隠蔽」を指していることです。
「流動的要素のカプセル化には関連とインターフェイスを積極的に使う」
以前、Java user group meetingで、Javaの発明者である James Gosling が講演をしたことがある。その時の印象的な質疑応答で、誰かが彼に尋ねた。「もし、Javaをもう一度やり直せるとしたら、何を変えますか?」彼は「クラスをやめる」と答えた。笑いが収まったあと、彼は「本当の問題はクラスよりも実装継承 (extendsの関係) だ」と説明した。インターフェイス継承 (implementsの関係) の方が望ましい。どんな時も実装継承はできるだけ避けるべきだ。
― 「Why extends is evil」5 (infoworld誌) (和訳)
2項目は、「 (クラスの) 継承を無闇に使ってはならない」 というアンチパターンとしてよく知られています。ここで継承と言っているのはクラスがクラスを継承する場合のことで、インターフェイスの実装やインターフェイス間の継承は指していません。クラスの継承が必ず問題を引き起こすわけではありませんが、クラスの継承が必要以上に強力で間違いを起こしやすく、クラスの継承特有のメリットも少ないため基本使うべきでないということです。クラス継承が流動的要素のカプセル化に寄与するケースは、継承という強力な機能からイメージされるよりも圧倒的に少ないと思ってください。クラス継承を使用したいほとんどのケースは関連とインターフェイスによって代替できます。
- is-a関係を作ってクラスをカプセル化したい。多相性 (ポリモーフィズム) を実現したい。
- → インターフェイスを使います
- 振る舞い (処理) の実装の一部を別の場所に切り出したい。
- → 別のクラスに切り出し、関連で連携します
- 関連による実装の結合はクラス継承よりDRY6ではないかもしれませんが、クラス継承で密結合なクラスを大量に生み出してしまうよりはかなりまともです。
- → またはインターフェイスに実装を置きます
- ただし置き場として適したインターフェイスが既にあり、インターフェイスでのメソッド実装が言語機能として許される場合
- → 別のクラスに切り出し、関連で連携します
クラス継承よりもオブジェクトコンポジションを多用すること。
― 「オブジェクト指向における再利用のためのデザインパターン 改訂版」 1.6 デザインパターンで設計問題を解く
- ここでの「オブジェクトコンポジション」はUML標準におけるコンポジションとは異なり、抽象的な意味での「関連」(集約、コンポジションを含む) を指しています。
GoF は優れたオブジェクト指向設計を生み出すための戦略を示唆しています。彼らが特に示唆しているのは以下のことです: - インターフェースを用いて設計する。 - クラス継承よりもオブジェクトの集約を多用する。 - 流動的要素を見つけ出し、それをカプセル化する。
こういった戦略は、本書で考察しているデザインパターンのほとんどにも採用されています。
― 「オブジェクト指向のこころ 第2版」 5.5 デザインパターンを学習するその他の利点
- ここでの「集約」も「オブジェクトを組み合わせる」という一般的な意味なので、本連載における「関連」に読み替えて下さい。
クラス継承という機能は様々に誤った使い方ができ本質的に何が悪いかという話は複雑なのですが、以下に私個人の考察を加えておきます。
クラスの継承がインターフェイスの実装や継承と比べて違う点は何でしょうか。クラスが最も具体的な実装を定義し得るのに対し、インターフェイスは、クラスの間にある非流動性 (共通性) を定義することに特化しています。インターフェイスは状態 (Javaでは主にフィールド) を持ちません。つまりインターフェイスは流動的要素のメインステージである、実行状態からの干渉を受けにくいのです。そのためインターフェイスの継承や実装ではその関係を境に流動性を一定程度分離できます。一方でクラス継承では、最も流動的な要素である「状態」を容易に他のクラスと共有できてしまうという危険性があります。少し足を踏み外せば、一つのクラスに対して単一であるべきはずの流動的要素が複数のクラスという静的な構造にまたがってしまい変更耐性が悪化します。これは直接的に状態を継承する場合に限りません。状態をフィールドとして直接継承しなくてもクラス継承は状態に依存した実装群を簡単に引き継げてしまうので、見た目以上にクラスを複雑にしてしまいます。継承の階層が深くなればなるほど危険です。
このようにクラス継承は流動的要素のカプセル化を破壊してしまう力を持っているので、使わずに済む場合は常に使わないべきです。
私の思いつく限りでは、以下のようなケースではクラス継承を導入できます。
- スーパークラス (基底クラス) をどうしても変更したくない/できない場合
- クラスの共通性 (非流動性) をインターフェイスで定義した際その共通性に本質的に状態 (データ) が含まれていたという場合
- この場合インターフェイスを抽象クラスに置き換えて注意深くprivateな状態を書き加えれば問題は起きません。例えばObserverパターンのobserverコレクションなどです。
- ただし代替案として、クラス継承の代わりにインターフェイス実装を使い、状態への (可変) 参照を返すメソッドをインターフェイス化する手もあります。機能的にはほぼ変わりませんがクラス継承の構文を避けることができるメリットがあります。ただしpublicメソッドによって内部状態への可変参照が取得できてしまうという気持ち悪さがあり、Javaなどでは慣習的にあまり許容されないかもしれません。
- Rustにはほぼインターフェイスに相当するTraitというものがありますが、Javaと異なり、Traitメソッドで可変参照を返す書き方は文化的に受け入れられます。Rustでは参照の可変性が厳密に管理されており、不変参照だと思っていたら意図せず可変参照を使ってしまった、という事故は防がれるからです。またパッケージの細分化とパッケージ単位でのカプセル化も多用され、Traitであってもパッケージで簡単に隠すことができます。
ただし次のことに気をつけます。
- サブクラスとスーパークラスの結合を弱くする
- サブクラスがスーパークラスの状態を意識しなければならないような設計は避ける。
- スーパークラスの中身をなるべくprivateにする。可視性の高いデータや振る舞いの実装をなるべく継承しない。
- 可能な限り階層を浅くし、複数の拡張を重ねない。実際「オブジェクト指向のこころ」によれば、Bridgeパターンや基本的な関連・インターフェイスを駆使すればクラス継承が2階層以上に及ぶことはほぼないようです。
- よく分からなければとりあえず使わない
「データと振る舞いの分離は禁止されない」
このルールも2つ目のルールと同様、古典的なオブジェクト指向の機能の不自然さを意識するためのものです。古典的なオブジェクト指向の機能だけではデータ構造と振る舞いを分離することが困難かつ不自然に見えてしまい、それが仕組み的な欠陥に起因していることを知らないと無駄に悩んでしまう ので積極的な注意が必要です。どのように困難であるか、どう対処するのかはVisitorパターンの章で説明します。
- ここで言うデータとは「基本的な演算・操作の対象となるオブジェクト」でありJavaでは変数・フィールドとして現れます。振る舞いとは「データを使って何をするか」でありメソッドが表現しています。「フィールド」「メソッド」と呼んだときに付いてくる特定の言語への依存や具体性を無視するために抽象的な言い方をしています。
- ちなみに関数型言語やモダンなオブジェクト指向言語ではしばしば 「直和型」と呼ばれるVisitorパターンと等価でより優れた構造がある ため、Visitorパターンを使う必要はありません。
私自身、これまでオブジェクト指向の設計で迷ったケースの多くがデータと振る舞いの分離に関する葛藤でした。オブジェクト指向ではデータと振る舞いを一体化させるべきなのだと誤解していましたが、実際には単体のデータ、単体の振る舞いもオブジェクトとみなすことができます。Visitorパターンがこのことをよく示しています。このパターンは「データ構造と振る舞いを (それぞれ多相性を持たせて) 分離する」パターンです。つまり、オブジェクト指向はデータとその振る舞いの一体化を強制するものではありません。データと振る舞いそれぞれに個別の流動性がある場合は分離することができます。
もし過去の私と同様に「オブジェクト」=「データと振る舞いを一体化させたもの」だと感じていた方がいれば、次のことによく注意してください。「オブジェクト」はデータと振る舞いを組み合わせたものだけを指しません。「オブジェクト」は基本的な値 (専門的な用語で言えば、第一級オブジェクト) として扱われるものであり、0個以上のデータと0個以上の振る舞いの組み合わせ です。つまり 「データと振る舞い」だけでなく「データ」や「振る舞い」もオブジェクト になりえます。
実際にデータ構造と振る舞いを分離するためには、クロージャが言語機能として強くサポートされていることが重要です。クロージャとは定義元の環境で評価される関数のことです。オブジェクト指向言語では、関数実行時に引数以外の変数を参照させたい場合、普通はその変数を持っているオブジェクトのメソッドとして定義します。つまり実行環境と定義環境が同じオブジェクトであるということです。ラムダ式と呼ばれるものはしばしばクロージャとしての機能を持ちます。ラムダ式を使うと、メソッド内のローカルな位置などで関数を定義し、その定義環境から見えるローカル変数などを関数実行時に参照させることができます。クロージャの使い方についてはVisitorパターンとIteratorパターンの章で述べます。
オブジェクト指向に必要な言語機能
あくまで後知恵ですが、これまでの結論からするとオブジェクト指向においてクラスとクラス継承という仕組みは改善の余地があるように思われます。オブジェクトは0個以上のデータと0個以上の振る舞いの組み合わせです。静的型付けにおいてはこのオブジェクトを静的に設計し契約を作るための何らかの構造が必要であり、古典的にはそれがクラスです。しかしクラスは1つの手段に過ぎません。データと振る舞いを静的に結合するためにクラスという柔軟性に欠けた結合方法を使う必要はありませんでした。クラスはデータと振る舞いの一体化のみを推進しデータと振る舞いそれぞれの扱いやすさを無視します。その柔軟性の無さを補うためにクラス継承という過剰に大掛かりな機能も生まれてしまいました。
クラス継承の役割は多岐に渡ります。データの共有、振る舞いの共有、そして多相性です。多相性はこれまで見たようにクラス継承の特権ではなくインターフェイスで実現できます。残りの2つ、データ・振る舞いの共有についてもデータと振る舞いそれぞれを静的かつ柔軟に扱えれば達成されます。つまり、データ型を他のデータ型に注入する機能 (代数的データ構造) と関数を他の関数に注入する機能 (関数オブジェクト、高階関数) があって簡潔に書けさえすればデータと振る舞いの共有・再利用は可能で、曖昧な文脈を丸ごと共有するクラス継承の仕組みは必要ありません。データと振る舞い (メソッド) を柔軟に扱えることを前提とすればクラスとはただデータとメソッドを寄せ集めただけのものであり、もはやデータとメソッドの”入れ物”ではありません。このような思想は例えばRustに見ることができます。Rustは従来のオブジェクト指向に強く影響を受けていますが、クラスという概念は使用していません。Rustではデータとメソッドの静的な結合は明示的に表現され、結合を何度でも自由に定義したりメソッドを後付けしたりすることができます。またインターフェイスに代わってデータが持つメソッドを契約する型としてトレイト (trait) というものが導入されました。これもいわゆるインターフェイスとは異なり後付けで実装できます。
データと振る舞いをそれぞれ柔軟に扱うための主流な方法としては、代数的データ構造に基づいた型システムと関数オブジェクト (値としての関数) を導入するというものがあります。Visitorパターンを置き換えられる機能である直和型も代数的データ構造の構成要素です。これらは関数型言語から輸入されたものです。
関数を他の関数に注入できれば柔軟に実装の共有ができると言いましたが、厳密に言うとこれは良くも悪くも明示的な共有です。一方クラス継承によって実装を継承するとき継承する側で明示的にどのデータやメソッドを継承するかなどを指定することはありません。つまりクラス継承はある程度暗黙的に文脈を注入できるという特別な特徴を持っていて、実際これは記述量が減り便利だという側面があります (クラス継承の危険性に比べれば大したことではありませんが) 。これと同等のことを実現するには単なる関数オブジェクトだけではなくクロージャが必要です。
- もし暗黙的な文脈を扱うという目的を押し進め、実装に対して文脈を外部から注入したいという話をするならばモナドやeffect systemといった関数型言語寄りの領域に発展すると思います。関数型プログラミングにはまだ実験的な部分も多いですが、関数型プログラミングの資産を少しずつ取り入れることでプログラミング言語ははより良くなれるのではないかと思っています。
次回予告
第2回は以上になります。今回でオブジェクト指向を解釈するための道具が揃ったので次回以降は実際に先人の残した素晴らしいデザインパターンを分析していきます。「流動的要素を非流動的要素によってカプセル化する」ことの分かりやすい例として、AdapterパターンやStateパターンを扱います。
採用情報
朝日ネットでは新卒採用・キャリア採用を行っております。
- 和訳本「オブジェクト指向における再利用のためのデザインパターン」では「1.7 どのようにデザインパターンを選択するか」にこの記述があります (ここでの引用文は、オブジェクト指向のこころの訳者が翻訳したバージョンになっています)↩
- このことを考慮すると流動性はより抽象的に「設計の一要素が、存在するまたは期待する複数の異なる実装に対応すること」と言えるかもしれません。設計要素に変更を想定するのはその要素を「将来ありえる複数の異なる実装」に対応させているということで、実装の巨大な繰り返しを見つけるのは「現在存在する複数の異なる実装」を繰り返し=一つの設計要素に対応させるということです。↩
- この点においても、私はオブジェクト指向を「カプセル化・継承・多相性」から定義するのは妥当でないと考えています↩
- 正確にはインターフェイスとクラスの関係は型の多相性の隠蔽=型の種類の隠蔽だけではなく型の詳細情報の隠蔽としても機能します。↩
- https://www.infoworld.com/article/2073649/why-extends-is-evil.html↩
- DRYであるとは、DRY原則 (何回も同じことするんじゃねえ原則) に従っている様。DRY = Don't Repeat Yourself↩