保守性と拡張性を高めるためのSOLID原則とシステム設計
第4話: 開放/閉鎖の原則(2)継承とポリモーフィズムの活用
(最終更新日:2024.09.19)
(絵が小さい場合はスマホを横に)
継承とポリモーフィズムを上手に使おう
継承とポリモーフィズムは、オブジェクト指向プログラミングで柔軟な設計を実現するための重要な要素だ。
これらを誤って使用すると、コードが複雑化し、保守が困難になる。
本記事では、継承とポリモーフィズムの良い例と悪い例を比較し、正しい使い方を理解する。
また、リファクタリングの具体例を通じて、システムの保守性と拡張性を向上させる方法を解説する。
[目次]
1.継承とポリモーフィズムの活用
継承とポリモーフィズムの活用は、オブジェクト指向プログラミングにおいて柔軟な設計を実現するための基本的な概念だ。
それぞれの役割と、それらを活用することでどのように柔軟な設計が可能になるかを解説する。
1.1 継承(Inheritance)
継承は、あるクラス(親クラスやスーパークラス)の機能を別のクラス(子クラスやサブクラス)に引き継ぐことだ。
これにより、子クラスは親クラスのメソッドやプロパティを再利用しつつ、必要に応じて新しい機能や動作を追加、
親クラスの動作をオーバーライドして独自の挙動を定義することができる。
下記では、Animalクラスを継承してDogクラスを作り、makeSoundメソッドをオーバーライドしている。
継承、オーバーライドの例
1.2 2. ポリモーフィズム(Polymorphism)
ポリモーフィズムは、同じメソッド呼び出しが異なるクラスで異なる挙動を取ることを意味する。
これにより、統一されたインターフェースを通じて異なる実装を扱うことができる。
ポリモーフィズムには、コンパイル時のポリモーフィズム(オーバーロード)と実行時のポリモーフィズム(オーバーライド)があるが、
ここでは主に実行時のポリモーフィズムを扱う。
来は、インターフェースを介して、そこから継承した2つの異なるクラスでポリモーフィズムを実現する。
Animalインターフェースを介して、それを継承したCatクラスとDogクラスを作成し、異なる鳴き声のするmakeSoundメソッドを作る。
これにより、既存のコードを変更することなく機能を拡張できる。
ポリモーフィズムの例
1.3 継承とポリモーフィズムの活用による柔軟性
継承とポリモーフィズムを活用することで、以下のような利点が得られる。
コードの再利用性:親クラスに共通の機能を定義し、子クラスでその機能を拡張することで、コードの重複を減らし、再利用性が向上する。
拡張性:子クラスを追加することで、新しい機能を既存のコードを変更せずに導入できる。
ポリモーフィズムにより、異なるクラスを同じインターフェースで扱えるため、柔軟に拡張できる。
可読性の向上:統一されたインターフェースを通じてクラスを扱うため、コードの可読性が向上し、システム全体を理解しやすくなる。
下記に継承とポリモーフィズムを利用した支払いシステムでの例を掲載する。
PaymentProcessorインターフェースを作成し、支払いプロセスのメソッド名だけ定義する。
これに対して、CreditCardProcessorクラスとPayPalProcessorクラス各々で、PaymentProcessorインターフェースをオーバーライドし、
processPaymentメソッドの具体的な実装を行う。
継承とポリモーフィズムを活用したクラス
これらのクラスのインスタンスを生成し、processPaymentメソッドを呼び出すことで、それぞれ処理の違うメソッドを呼び出すことができる。
支払いシステムでの活用
このように、継承とポリモーフィズムを活用することで、異なる支払い方法を同じメソッド呼び出しで処理できる。 統一されたメソッドで、システムを柔軟に拡張できるようになる。
2.良い例と悪い例
ここでは、継承やポリモーフィズムをどのように適用するかについて、正しい設計(良い例)と誤った設計(悪い例)を具体的に説明する。 これにより、設計上のベストプラクティスと避けるべきポイントが明確になる。
2.1 悪い例: 継承を誤って使った設計
継承を乱用してしまうと、クラス間に不必要な依存が生まれたり、コードの変更が難しくなる。以下は、その悪い例だ。
下記のコードだと、新しい支払い方法が追加された場合、PaymentProcessorクラス自体を変更しなければならず、
既存のコードに悪影響を与える可能性がある。また、支払い方法ごとに新しいメソッドを追加していくため、
クラスが巨大化し、将来的に保守が難しくなる。クラスが変更に対して閉じておらず、拡張時に毎回変更が必要なる。
bankTransferProsessor(銀行振込に対応した支払い)が増えた際に、PaymentProcessorを継承して新たなクラスを作ったとしても、
結局どのクラスを使えば良いか分からず、悪い継承方法と言える。
悪い例
2.2 良い例: 継承とポリモーフィズムを正しく使った設計
継承とポリモーフィズムを正しく使うと、新しい機能を追加する際に既存のコードを変更せずに柔軟に対応できる。
下記では、異なる支払い方法をポリモーフィズムを活用した。PaymentProcessorインターフェースを継承して3種類の支払いクラスを作成している。
このような作り方だと、新しい支払い方法を追加する際、PaymentProcessorインターフェースを実装した新しいクラスを追加するだけで済み、
既存のコードに変更を加える必要がない。拡張に対して開かれており、変更に対して閉じるOCPに準拠している。
インターフェースを使って正しく継承した例
下記の使用例だと、PaymentServiceクラス内で各々の支払い方法に応じたクラスのインスタンスを生成しており、 それらの支払い方法のクラスを引数として用いることで、paymentService.Processメソッドを用いたとしても、異なる支払い方法を実現している。 支払い方法の実装が変わっても呼び出し方法は一貫していて、柔軟性が向上する。コードの再利用性も高い。
良い例の使用方法
2.3 悪い例と良い例の比較
良い例と悪い例を表にまとめる。
項目 | 悪い例 | 良い例 |
---|---|---|
拡張性 | 新しい機能を追加するたびにクラスを変更しなければならない | 新しい支払い方法を追加しても既存のコードを変更せずに済む |
再利用性 | 支払い方法ごとに個別のメソッドを持つため、コードの重複が発生しやすい | インターフェースで統一されたメソッドを使うことで、コードを再利用しやすい |
保守性 | クラスが巨大化し、修正が複雑になる | 各支払い方法ごとに独立したクラスで処理されるため、保守が容易 |
OCP | クラスは変更に対して閉じられていないため、変更のたびに既存のコードを編集する必要がある | 拡張に対して開かれており、既存のクラスを変更せずに新しい機能を追加できる |
ポリモーフィズム | ポリモーフィズムが使われていないため、コードが一貫性を持たない | インターフェースを使用して異なる実装を共通のインターフェースで扱える |
2.4 まとめ
悪い例では、クラスが一度に複数の責任を持ち、変更時にコード全体に影響を与えるため、拡張性と保守性が低下する。
良い例では、継承とポリモーフィズムを適切に利用しており、コードは柔軟で拡張しやすく、
既存のコードに変更を加える必要がないため保守が簡単になる。
3.まとめ
今回は開放/閉鎖の原則(OCP)の第2回として、継承とポリモーフィズムを使った支払い処理について解説した。 悪い例として、各支払い方法を1つのクラスで管理してしまい、コードが複雑化したものを紹介した。 これに対して、PaymentProcessorというインターフェースを導入し、各支払い方法を別々のクラスで実装することで、改善を図った。 そして、ポリモーフィズムで支払い方法を柔軟に切り替えられるようにした。 さらに、リファクタリングでは、コードを分割し、単一責任を持つクラスにすることで、保守性と拡張性を高めた。 今回行った手法を真似することで、保守性と拡張性の高いコードを開発しよう。