- .NET 8
データそのものやそのデータに対する処理を中心に考える時、そのデータの構造の事を オブジェクト と呼び、 このオブジェクトを中心にプログラムを設計していく手法を オブジェクト指向と呼びます。
オブジェクト指向で作る事で、変更に対して柔軟になります。変更されやすい箇所をオブジェクトとして切り出す事ができます。 実装したい機能を単体で作り、それをパーツを組み合わせるようにアプリケーション開発ができるようになります。
例として、通知機能を考えてみましょう。
通知を行う方法は色々あります。メール、チャット、SMSなどたくさんあります。他にも色々増えていくかもしれませんので、これはオブジェクトとして切り出しておくべきだと考える事ができます。
しかし、通知機能自体を考えるとどうでしょう?"通知"の本質としては、メッセージを誰かに伝えたい
です。
であれば、伝えたいメッセージをオブジェクトに与え、メッセージを受け取ったオブジェクトは、各々の方法で通知機能を実装して処理することで、
メッセージをメールとか、チャットとかで送る事ができるようになります。
通知機能を使いたい側(実装側):「このメッセージを送ってくれる?」
通知機能(処理側):「OK。」
こんな感じですね。
こうなると各々の責務が明確になります。責務が明確になれば見やすいものになり、見やすいという事はメンテナンスしやすいものとなります。
重要なのは責務を明確に分けるという事です。それぞれのオブジェクトをできる限り疎結合に保つ事で依存性が無くなり、結果として変更に強くなります。
変更に柔軟になる。これがオブジェクト指向で作る理由です。
密結合=依存性が高くなるとちょっとした変更が色んなところに影響し、ちょっとした変更がシステム全体に影響し、思わぬ不具合を招く事になります。
通知機能を修正しただけなのにファイルのアップロード部分に不具合が出るみたいな事も起こりかねません。
大きく3つ
- カプセル化
- 継承
- 多様性(ポリモーフィズム)
車を構成するパーツはエンジン、ハンドル、ブレーキの3つとします。 それらを組み合わせて車を作ります。
エンジン | ハンドル | ブレーキ |
---|---|---|
始動する | 右に曲げる | 踏む |
停止する | 左に曲げる | 離す |
回転数(プロパティ) |
上記がデータ構造です。
一言にエンジンと言っても世の中には沢山の種類のエンジンがあります。
ガソリンで動くものもあればガスで動くものもあり、レシプロエンジンとロータリーエンジンでは内部構造も全く異なります。
それらすべてに沿って処理を書いていくとそれぞれのエンジンの特性に応じて条件分岐とかを書く必要が出たり、エンジン側の実装が変更になった場合、処理の書き換えが必要になったりと大変なことになります。
なので、エンジンの構造がどうなっているのか、どういう原理で動いているのかを隠蔽し、
始動と停止の呼び出し部分だけを外部に公開し、利用者は公開された処理を呼ぶだけで使えるようにします。
これをカプセル化と呼びます。この仕組みにより、エンジンの複雑な仕組みを知らなくても多くの人がエンジンを扱うことができます。
エンジンがどういう仕組みで回り、どうやって力が伝わり前が進むのかを知らなくても、「アクセルペダルを踏めばエンジンの回転数が上がって離せば下がる」事を知っておけば車は運転できますよね?それと同じです。
親クラスの機能を受け継ぐ機能として継承があります。
よく親クラスからの機能受け継ぎとして解説される事が多いですが、本質はそこではありません。
親クラスを共通点として引き継ぐ事で、規格に沿ったクラスを作ることができるのが、継承の本質です。
今回の例だと、Engine.cs
が親クラスとなります。見ての通り抽象クラスであり、親クラス自体に機能は定義していません。
これはあくまで親クラスは共通点を定義しているだけだからです。機能自体はこれを継承する子クラスで定義します。
こういう設計にすることで、
- エンジンを使う側:始動する
(Onメソッド)
、停止する(Offメソッド)
を呼ぶだけでエンジンを扱える。エンジンの内部構造を知る必要がない。 - エンジンが使われる側:始動する処理が実行されたらエンジンを動かす処理を定義し、停止する処理が実行されたら安全にエンジンを止める処理を定義すれば良い。
と、抽象クラスを定義して処理を子クラスに実装するという設計にすることで実現できるのが、ポリモーフィズムです。
実際の処理を実装したクラス(今回の場合だとEngine
クラスを継承した具象クラス)ではなく抽象クラスを使用して実装を行う事で、抽象クラスを継承すればどんなものでも動かす事ができるようになります。
抽象クラスで定義したメソッドを呼び出すように処理を実装する事で、抽象クラスを継承した子クラスの処理を呼び出す事ができます。
継承したクラスが変わったとしても抽象クラスで定義したメソッドを呼び出すように作っておけば変更の必要が無くなります。
同じメソッドを呼んでいるけど異なる処理を使用する事ができる。これがポリモーフィズム(多様性)です。
例えば、エンジンを別のものに載せ替えたいとします。今まで使っていたものとは構造も燃料も違います。
抽象クラスを使わなければ、エンジンの構造と動かし方を理解し、定義したメソッドを呼び出すように実装を変更しないといけません。
しかし抽象クラスを継承しておけば処理を全く変更することなくエンジンを載せ替える事ができます。
では実際に実現してみましょう。
// こうなっていたとして、エンジンを載せ替えたいと思っても?
Engine engine = new JetEngine();
Handle handle = new QuickHandle();
Brake brake = new AntilockBrake();
Car car = new Car(engine, handle, brake);
// ReciproEngineクラスがEngineクラスを継承しておけば、インスタンスを差し替えるだけで載せ替え完了。
Engine engine = new ReciproEngine();
Handle handle = new QuickHandle();
Brake brake = new AntilockBrake();
Car car = new Car(engine, handle, brake);
もちろん、ReciproEngine
クラスについては、継承した親クラスの定義の通りに始動と停止の処理を作ってあげればOK。
後はCar
クラスの中でEngine.On()
メソッドを呼べば、JetEngine
だろうがReciproEngine
だろうが関係なく動かす事ができます。
このように、具体的に処理を実装したクラスに対してではなく、抽象化したクラスに対して処理を実装していく事でポリモーフィズムが実現できます。