なぜオブジェクト指向で書くのか
オブジェクト指向のメリットは、以下の3つに要約できます。
- 可読性の向上
- 拡張 変更への対応の柔軟化
- 再利用可能なパーツの部品化
これから説明するオブジェクト指向の三種の神器やSOLIDの原則で説明する原則は全て、この3つのメリットを実現するためにやってることだと意識してください。
メリットが得られないのであれば、わざわざオブジェクト指向で実装する必要はありません。
OOP三種の神器
- カプセル化
- 継承
- 多態性
はOOPの三種の神器と呼ばれているらしいです。
これは、オブジェクト指向の基本として非常に有名なので、そんなの知ってるよと思われるかもしれませんが、
どのようなメリットがあって、どのように実装するべきなのかについて話します。
カプセル化
カプセル化というのは、複雑でわかりにくいものを、そいつが持ち合わせるメソッドを用いて、簡単にやりたいことを実現する仕組みのことです。
カプセル化されたものというのは、仕組みがわからなくても使い方がわかれば使うことができるようになっています。
もっともわかりやすい例は、パソコンだと思っています。
パソコンというのは、多くのレイヤーに分けられカプセル化を繰り返して実現したものです。
もしもパソコンが一切カプセル化されていない状態、
つまり、GUIは当然なく、CUIすらない状態であったら使い物にならないと思いませんか。
Twitterを開くだけでも、毎度毎度HTTPリクエストを作り、それを信号化して送信する必要があります。
送信先もわかりませんね。
車だってそうです。
エンジンをつけてアクセルを踏めば前に進みますが、これは車がもつアクセルというメソッドを呼ぶことによって前に進んでいます。
このようなカプセル化がされていなかったらどうするのでしょうかね。
多分おそらくとても高度な知識とも労力がないと前に進まないでしょうし、一度進んだら止めることはできないでしょう。
つまり、使い物にならないんですよ。カプセル化されていないものというのは。
カプセル化された使えるクラスというのは、
メソッドの引数と返り値さえ知っていれば使えるクラスということになります。
そのようなクラスを目指してください。
そのためには、クラス名とそのメソッド名が明瞭なものであることが求められます。
名前が分かりやすいということを重要としないプログラマーはいませんよね。
それは、プログラムというのは、書いている時間の何百倍もの時間読んでいるからです。
僕がプログラムを書いている間自分の書いたコードを読みながらでないと次のプログラムがかけません。
僕の同僚が僕の書いたクラスに機能を追加するときには、僕のコードをどうしても読まなければいけません。
もしも僕の書いたコードの変数や関数の名前が分かりにくかった場合、
多くの読者に混乱やストレス、分からない箇所の解析や調査の必要性といった非効率性やボトルネックという損害を与えます。
また、誤読が生じてしまった場合にはシステムそのものを破壊してしまうかもしれません。もしくは、変更を加えることそのものが不可能になってしまうかもしれません。
だから、適切なクラス名メソッド名をつけること、適切なクラスに適切なメソッドを持たせることが非常に重要なのです。
そして、僕が今ここに語ったことがカプセル化の全てです。
getter/setterについて
カプセル化というとgetterとsetterのことだと思っている人がいるので、それについても言及します。
確かにgetterとsetterもカプセル化の1つです。
クラスのプロパティを気にせずメソッドだけで求めたい値を取り出し、渡すことができるからです。
仮に、setterを使わなかったとしたら、継承先のメソッド内から、親クラスで宣言・定義されているプロパティへ直接変更することになるので、一切validateも効かないし、少なくとも継承先のプロパティの使われ方を気にしなければいけませんよね。
間違った型を突っ込んだり、正の値しか受け取らないところに負の値をいれてみたり、配列を持つところにvalueをいれてみたりしたらちゃんと弾かれるもしくは修正されるようにsetter内部で修正すべきです。
しかし、これって本当にカプセル化されていますかね。
実際は、クラスのプロパティとその仕様に大きく依存してしまっていると思うのです。
だから、本来は、ある処理をする際、その処理に必要な情報をオブジェクトから引き出さないで、情報を持ったオブジェクトにその処理をさせた方がカプセル化の原則が守られるのです。
そうすれば、基本的にgetter/setterを使う必要がある場面は無くなります。
継承
一般に、親クラスの機能を受け継ぐ機能だと思われています。
しかし、一般的なオブジェクト指向の言語には「継承」には4つのものを継承する機能が備わっていることが多いです。
- superクラスの継承
- コンポジションの継承
- interfaceの継承
- 抽象クラスの継承
この4つの継承方法の使い分けが継承においては非常に重要になります。
これはつまり、クラスとクラスの関係を表すものですから、これがしっかりと分けられることがこの後説明する多態性につながります。
superクラスの継承
結論から言えば、ほとんど使うことがありません。いや、一切ないと思います。
なぜなら、他の3つの継承の方法で十分であるからです。
もっと言えば、superクラスの継承を行うと親クラスに用意してあるフィールドメンバとメソッドと子クラスのインスタンスとの疎結合性が保たれなくなります。それだけでなく、親クラスと子クラスの名前空間が合体して名前空間も複雑になりますね。
継承をすることはデメリットしかないのではないかと思います。
オブジェクト指向は難しいですが、親クラスの継承だけは簡単に理解できるため親クラスにまとめることがオブジェクト指向とでも思ったのか、
superクラスにビジネスロジックを書き上げるRailsのコードを読んだことがあります。(自社ではないですが)
親クラスというのは抽象に対するクラスですよね。
抽象に対するクラスは如何なる時であっても、具象を意識せずに書かなければならないんです。
なぜかというと、具象側はビジネスロジックや外部環境の変化によって定期的に更新の必要が出てきます。
その時に、親クラスが具象を意識したコードになっていると、抽象クラスを変更しなければなりません。
それはそれは大問題ですね。
だって、その親クラスを引き継いでいる全てのクラスに支障が生じるからです。
なので、抽象を表す親クラスは絶対に具象を意識してはいけません。
で、あればコンポジション、抽象クラス、インターフェイスで十分というか、そのほうが適切ではないですか。
親クラスを使う時というのがあれば、それはただ1つだけ、その親クラスの子クラスの変更では、絶対に変更されない部分だけを切り出した時に余りにも大量のプログラムがある時です。
その時は、親クラスとして抜き出しましょう。
そうすると、書くべきプログラムの量が大幅に減りますね。
コンポジションの継承
コンポジションというのは、車のパーツにハンドルがあって、タイヤがあって、アクセルがあってとつなぎ合わせていくようなものです。
public class Car{
private handle;
private tire;
private body;
public Car(Handle handle, Tire tire, Body body){
this.handle = handle
this.tire = tire
this.body = body
}
}
car = new Car(handle, tire, body)
まぁ、こんな感じです。
ただ、いや、車にもスーパーカーと普通自動車があってそれによって、速度を取得するメソッドも違うし、運転手も選ばないといけないんだよ。そうなったら、Carを親クラスにして。。。
なんていう人がいるかもしれません。
そう言った場合に重要になる考え方がこれから示す多態性という考え方です。
この考え方が多態性の考え方そのものです。
あくまでも使うべきは、コンポジションによる継承なのです。
多態性
結局上のような場合はどうすればいいのかというと、
まずはCarという抽象的なクラスを作ります。
ここでのCarはあくまでも概念としての車であって乗ったり運転したりすることはできません。
ありとあらゆる車に共通するものを取り出した車という単語そのものの概念を表すクラスです。
public class Car {
private CarType car_type;
private CarDriver car_driver;
public Car(CarType type, CarDriver driver) {
this.car_type = type;
this.car_driver = driver;
}
public some_method(){
this.car_type.some_method()
}
}
public class NomalCar implement CarType {
public some_method(){
...
}
}
public class SuperCar implement CarType {
public some_method(){
...
}
}
public interface CarDriver {
...
}
public class NomalDriver implement CarDriver {
...
}
public class SuperDriver implement CarDriver {
...
}
ここでは、some_method()という関数をスーパーカーと普通の自動車で内容を分けたいとした時に、このような実装をします。
これは、分けるべき関数をinterfaceとして定義して、それを実装(継承)した先で具象のメソッドを書いています。
それをコンポジションとして呼び出し、実際に使うのはより抽象的なCarクラスのインスタンスが使うことになります。
これが多態性が意味する抽象化の形そのものです。
得られるメリット
この形にすることで、新しくトラックについてのsome_methodを書く時にも柔軟に拡張できますし、
スーパーカーについてのドライバーの規定に変更があったとしても、具象側のSuperDriverのクラスの変更を設定することで変更できます。
継承を用いて抽象のCarクラスにあらゆるメソッドを持たせていた場合にはそのようなことはできません。
ポリモーフィズムでは抽象に対してメソッドを要求することで、車を買い換えてSuperCarになりましたと言っても実装がCarクラスに対して定義されているので、非常に簡単に変更が聞きます。
これを書く具象クラスのインスタンスとして定義していたら全て書き直しです。
実装のポイント
まとめると、ポリモーフィズム実装のコツは、
- 抽象から条件分岐を減らすこと
- 抽象に対してリクエストすること
の2つです。
意外と知らないクラスの挙動
クラス情報
クラス情報とは、クラスごとのプロパティメンバやメソッドメンバとかの各クラスに付随する情報です。
これは、実は意外かもしれないんですけど、静的変数領域に確保されます。
静的変数領域に確保されるんですけど、Javaを含む多くのプログラミング言語では、実行時に逐次ロードしています。
それは変なんじゃないかって話ですよね。
クラスって複数のサブルーチンからアクセスできるような構造になってるじゃないですか。
だから、静的変数領域に実行時にロードするのがメモリに一番やさしいんでしょうね。
ちなみに、実行時にクラスをロードするのですが、そのタイミングで他のクラス情報との関連付けも行われます。
つまり継承関係だったりしたら、そいつもそのタイミングで静的変数領域に呼ばれるということです。
逆に、インスタンスを宣言するタイミングでは、当然ヒープ領域が使われるわけですが、そのタイミングでは静的コード領域は参照する必要がないということになりますね。
インスタンス生成
先ほども言いましたが、インスタンス生成はヒープ領域に行われます。
複数のインスタンスを生成した場合には、全く別のところにもう一つ同じようなインスタンスを作ります。
C言語などでは御法度とされているようなヒープ領域を湯水のごとく使うような動作ですね。
時代の変化とともに、メモリがどんどん小さく安価になってきていることの影響だと思われますが、OOPで書かれたプログラムは基本的にインスタンスの持つ変数をヒープ領域に宣言したインスタンスの数だけ持っているということは、知っておいてください。
ちなみに、メソッドメンバはヒープ領域には読み込まれません。
静的変数領域に格納されたクラス情報を参照しています。
また、インスタンスの変数はインスタンスのポインターを持っています。
これは、関数へ渡されたりローカル変数として使われるときはスタック領域に配置されるからです。そのときインスタンスはヒープ領域に配置したいのでポインターを使って接続している格好になります。
終わりに
これからちゃんとした設計やアーキテクチャについても書こうかと思っていますが、一旦基本的なオブジェクト指向の考え方についてQiitaにまとめてみました。
設計やオブジェクト指向は奥が深いので、この記事を読んだ方の中にも色々思うところがある方もいらっしゃると思いますので、是非コメントの方でご意見等ございましたらお願いいたします。