対象読者
- 実装のない抽象メソッドを使う意味が分からない方
結論
私はオブジェクト指向初学者の頃、抽象メソッドを使う意味が分からなかった。だって、抽象メソッドには中身がないから。。。
そう思ってた時期が私にもありました、はい。
しかし、今では抽象メソッドをよく使うようになった。抽象メソッドを使うことでコード量は増えてしまう。しかし、クラスの責務を
空っぽの抽象メソッドが無駄に見える
抽象メソッドとは、シグネチャ(メソッド名や引数、戻り値)が決められただけの中身がないメソッドのことだ。
抽象メソッドを持つクラスを「抽象クラス」と呼ぶ。この抽象クラスを継承した具象クラスで抽象メソッドの中身を実装する。これが抽象メソッドの使い方だ。
抽象クラスを継承しても結局はその中身を実装しなければならない訳だ。クラスを余分に作らなければならないし、無駄にコード量を増やすだけではないか?
抽象クラスなんて用意せずに、それぞれのクラスで同じシグネチャのメソッドを実装すればよいではないか?
クラスが太っていく…
クラスの第一の意義は、関連する変数と関数をまとめることによって、コードをスッキリさせることだ(詳しくはこちら)。大量のファイルをフォルダ分けすると管理しやすくなることと同じだ。
これだけでもクラスを使うメリットはある。しかし、オブジェクト指向の豊かな表現力を発揮するには不十分である。なぜなら、関連をまとるという視点だけでは、意味合い的には無関係なものもクラス内に押し込んで、クラスが理解しづらくなってしまう恐れがあるからだ。
コードをクラスに分割することで可読性・柔軟性を高められる。それらのクラスを組み合わせて動作するコードを作成することがオブジェクト指向のやり方だ。しかし、クラスを組み合わせることと関連は混同されがちだ。
例えば、クラスAとクラスBを組み合わせてある動作を実現するとしよう。組み合わせ方は実に多様だ。
//例は後で考える
このように、1つのクラスは肥大化していき、遂には多様なプロパティやメソッドを兼ねそろえた便利クラスが誕生する。これは「神クラス」と呼ばれる。なぜなら、そのクラスは
- 全知全能である
- 複雑すぎて、神にしか理解できない
からだ。
こうして、コード分離の仕組みであったはずのクラスが本来の役割を果たさなくなり、大量のグローバル変数と関数が羅列された「手続き型プログラミング」と同じ世界へと逆戻りしていく。
責務
クラスが太らないように、クラスをシャープに保つ必要がある。そのためには、より積極的にクラスへ意味付けを行い、関連とクラス同士の組み合わせを区別しなければならない。そこで、クラスの構成要素(メンバ)を限定するための考え方が「責務」だ。
責務(Responsibility)と言うと難解な印象を受けるが、要は特定の要求に対して応答(Response)する能力(Ability)のことだ。責任や役割、担当とも言い換えられる。プログラムの語彙で言い換えれば、
- 要求とは、クラスの使用者(クライアント)がメソッド呼び出しを行うこと
- 応答とは、そのメソッド名に相応しい振舞いによってクライアントの期待に応えること
である。
1つのクラスは1つの責務を表現するものだと捉えることで、小さくて理解しやすいクラスが作成できる。
「この仕事(メソッド)はオレ(このクラス)の担当」というよりは逆に、「この仕事はオレ(このクラス)の担当ではない」ってことが大事だ。このメソッドはどのクラスに属するべきなのだろうか。プログラムの振舞いを変えずに設計を変更することを「リファクタリング」と呼ぶ。リファクタリングではメソッドをクラス間で移動させることが頻繁に行われる。
責務を念頭におくことで、そのメソッドの相応しい置き場所を模索するようになる。「このクラスはいろんなことをし過ぎていて、何かが間違っている。あぁ、このメソッドは複数の責務が絡み合っているな。では、もう一つのクラスを用意して、そちらに処理の一部を移動しようかな。同時にプロパティも移動すべきかな。この情報(プロパティ)はこのクラスが知っているべきことかな?」みたいな。
クラスに唯一つの責務を負わせるための絶えざる努力によって、美しい設計(小さく理解しやすいクラス群)が実現される。
抽象メソッド(クラス)は責務を明示する
以上より、責務はクラスの振舞い(メソッド)によって定義されることを確認した。
料理をしない料理人なんていない。デュエルしない者はデュエリストではない。クラスが何者なのかは行動(メソッド)によって規定できる。
抽象クラス(抽象メソッドを持つクラス)はサブクラスに抽象メソッドの実装を強制することにより、サブクラスに確実に責務を負わせる。こうしてエセデュエリストが存在する可能性をなくすことができる。
抽象クラスを使わずに、クラス名(○○Visualizerとか)によっても、責務を表現することはできる。それぞれのVisaulizerに共通のインターフェイスを持つメソッド(visualize)を実装することでも、抽象メソッドを実装した場合と同じことができる。
しかし、名前によって責務を表現しただけでは、本当に責務を果たすようなメソッドを持っているかは保証できない。プログラマの判断に依存してしまうからだ。抽象メソッドを使えば、プログラムの制約によって、確実にクラスに責務を負わせることができる。visualizeしてくれないVisualizerは存在する余地がなくなる。これが結論で書いた「抽象メソッドは責務(役割)を表現する」の趣旨だ。
制約によるプログラミングの進化
制約を設けることにより、プログラミングは進化してきた。以下に例を列挙する。
- 定数は変数の更新を禁止することにより、より安全にプログラミングを行えるようにする。
- gotoレスプログラミングはgoto文を禁止し、構造化文によりプログラムを表現することで可読性に優れたプログラミングを実現する。
- 関数型プログラミングは副作用を禁止することにより、バグを排除しやすくする。
抽象メソッドによる制約は、サブクラスのメソッドのインターフェイスを固定することである。この制約により、そのサブクラスを使用する際、振舞いが予測できるため、安心して仕事を依頼することができる(ポリモーフィズム)。
ポリモーフィズム(多態性)とは
ポリモーフィズムは、サブクラスのカプセル化である。同じインターフェイスのメソッドを持つサブクラスならば、どれでも同様に動作させることができる。ポリモーフィズムを実現したクラス群に対しては、どのサブクラスなのか具体的に知る必要はない。故に、制御プログラムの変更なしに、サブクラスを置き換えることができる。
現実世界から見るポリモーフィズムの例
レストランの責務は、顧客に料理を提供することだ。具体的な料理や調理方法は変更されやすいが、料理を提供するという点は変更はない。
料理を提供する責務を持つ抽象クラス(料理人)には,「料理を作る」、という抽象メソッドをとりあえず持たせておく。このメソッドは、材料を引数として渡すと、料理を返すというインターフェイスを持つとする。
具体的な料理人はプロの場合もあるし、業界未経験のアルバイトかもしれない。その調理方法(実装)は超絶テクニックによる調理かもしれないし、レンジでチンかもしれない。
抽象クラス(料理人)を継承しているならば、具体的な方法は分からないが、必ず料理を作ることが保証される(ポリモーフィズム)。
コメント