オブジェクト指向の誤ったアプローチについて思ったことを、生成AIを利用してまとめる
導入
前回オブジェクト指向について思ったことを、生成AIを利用してまとめました。 satoazuki.hatenablog.com
オブジェクト指向は、優れたソフトウェア設計を実現するための強力なツールですが、誤ったアプローチによってはアンチパターン(設計上の誤り)を生み出す可能性があります。オブジェクト指向におけるアンチパターンは、手続き型プログラミングよりも制御を複雑にする設計を招く問題が背後に潜んでいます。
このページでは、オブジェクト指向におけるアンチパターンに焦点を当て、それが背後にある問題について探求します。アンチパターンは、正しい設計原則から逸脱し、ソフトウェアの保守性、可読性、拡張性を損なう可能性があります。では、どのようなアンチパタンが存在し、それらがどのような問題を引き起こすのか、詳細に見ていきましょう。
派生クラスと条件文の増加
オブジェクト指向プログラミングにおいて、派生クラスを活用することは一般的です。派生クラスは基底クラスから継承され、独自のふるまいを提供します。しかし、時にはこの設計アプローチが問題を引き起こすことがあります。
派生クラス固有のメソッドを公開利用する設計
ある派生クラスで固有のふるまいを実装し、それを公開したくなることがあるでしょう。おそらくインターフェースクラスのメソッドとは無関係な、その派生クラスの内部状態に固有の利用ケースしたいのだとおもいますが、これは問題を引き起こす可能性があります。
ダウンキャストと条件文の増加
派生クラスが基底クラスと異なる操作を提供するためには、インターフェースクラスのインスタンスが派生クラスから生成しているかを実装者が知る必要があります。 そのためにはインターフェースクラスのインスタンスに対して派生クラスにダウンキャストできるかどうか調べる必要が増加し、コードの制御が複雑化します。
ダウンキャスト条件文を作らない
まずはインターフェースクラスのメソッドに抽象的に実装できるかどうか検討しましょう。 また、基底クラスが派生クラスの固有処理を適切に委譲できるように設計を見直すことも重要です。
条件文が増加する例
interface InterfaceClass { bool Convert(int input); } // 派生クラスの定義 class DrivedClass : InterfaceClass { public bool Convert(int input) { // メソッドの実装 return false; } public void Change() { // メソッドの実装 Console.WriteLine("Change method in DrivedClass"); } } class Program { static void Main(string[] args) { InterfaceClass instance = new DrivedClass(); // DrivedClass にキャストできるかチェック if (instance is DrivedClass derivedInstance) // ダウンキャストできるか条件 { // Change メソッドを呼び出す derivedInstance.Change(); // 派生クラス固有のメソッドをわざわざ読んでいる // Convertメソッドを呼び出す var doesGet = derivedInstance.Convert(42); } } }
継承による処理の見通しの悪化
オブジェクト指向は全体的な手続きと、末端のふるまいを切り分けることによって、可視性やカスタマイズ性を向上させる方法である。特に、全体的な手続きは入力から出力までのデータの流れを抽象化するので、昨今流行りのDXのために役に立つ。 一方で、末端のふるまいを手続きから切り離すということで、設計図が離れてしまう。設計が一か所にまとまっていないという点で見通しが悪化すると主張したい (もちろん、本来このオブジェクト指向手法によって得られる可視性やカスタマイズ性の恩恵が十分にでかい)
実行時の不透明な処理決定
オブジェクト指向では本来無視できる悪化を挙げた。しかし、この悪化の効果を劇的に上げるパターンがあり、それがクラスの継承になる。
共通な公開関数を基底クラスに実装するアンチパターン
クラスの継承を見かけるときは、たいてい今までのクラスのふるまいの一部を変えたいときである。
interface InterfaceClass { bool Convert(int input);] bool Revert(int input); } class BaseClass { public bool Convert(int input) { Console.WriteLine("Convert method in BaseClass"); return true; } public bool Revert(int input) { Console.WriteLine("Revert method in BaseClass"); return true; } } // 派生クラスの定義 class DrivedClass : InterfaceClass { public bool Convert(int input) { Console.WriteLine("Convert method in DrivedClass"); return false; } }
実行時に処理結果の不透明さ
上の例のような設計したとき、DrivedClass クラスをインスタンス化しそのメソッドを呼ぶと、その実装がBaseClassのふるまいをするか、DrivedClassのふるまいをするか実行時しかわからなくなる。 デバッガーは、①抽象化された手続き②ベースクラス③派生クラス、すべてのソースコードについてブレイクポイントをしかけ、そして実行して止まることを願うかステップ実行ですべての手続きを見ることになる。 (伝統的な手続き型コードであれば一つのファイルさえ注視すればよかった)