オープンクローズドの原則とは
「機能を拡張する」(オープン)と「修正を行わない」(クローズド)という一見矛盾するこの二つを同時に要求する内容のこと。
要は。。。
モジュールに新たな振る舞いを追加する際に既存コードを修正せずに新しいコードを追加するだけで目的を達成できる状態になっていることです。
全ての場合に適用するべきなの?
バリエーションを起因する場合のみが適用の範囲となる。
なぜこの原則を守った方が良いのか?
既存のクラスにif文を追加していく形だと元のクラスも再度問題ないかテストをしなければならなくなります。拡張した部分のクラスだけテストすれば良い状況が実装工数的には望ましいためです。
また、何か新システムにバグがあったときにこの原則を守っていればすぐにリカバリも可能です。
準拠させるポイント
- バリエーションによって変化する部分はどこか
- バリエーションの軸に沿ってまとめられているか
バリエーションの軸に沿ってクラス化する。
例えば、「フォームの種類」のバリエーションが多く増えるのであれば、それごとにクラスを分ける。
よく開発時に対象となりやすいバリエーションの例
インフラ層であれば
- データストア(Oracle、SQLServer、NoSQL、CSV、他のPCとの通信など)
- データストアのバージョン(Oracle11、Oracle12など)
ドメイン(業務)層であれば
- 会員ごとに処理を変えるケース
- 決済手法ごと(現金、キャッシュカード、PayPayなど)
- フォームの種類ごとに処理を変えるケース
概念
オープン
機能を拡張できる。
クローズド
修正を行わない。
オープンクローズドの原則の注意点
扱う業務や場合によっては、この原則に愚直に従うと非常にクラスの数が増えすぎてしまいますし、コードの可読性が下がります。
使うなら適量を守って使う。
適量を守って使うことが重要でしょう。例えば以下のようなケースではインターフェースにする必要もないのでオーバースペックな実装であると言えるでしょう。(アジャイルソフトウェアの奥義では「早まった抽象化をしないことも抽象化をすることと同じくらい大事」とも書かれています。)
- インターフェースがあるのに実装クラスが一つしかない場合
あえてオブジェクト指向を使わないという選択肢
基本的な処理のアルゴリズムは同じで固定値だけ変わるなどと行った場合は経験上無理にオブジェクト指向は使わずにバリエーションをコレクション(Map)を使って実装する方が良いです。(あるバリエーションでは1%、あるバリエーションでは5%など程度の差分しかない場合などでそれが何十個もあるケースなど)
なぜなら、こうした固定値の値とかはバリエーションの数が多すぎるのでソースの数が増えすぎるためです。
Mapに対して一つのクラスで処理を記述する方がクラスの数も増えず、条件分岐も増えずにスッキリしたソースになります。
呼ぶ側の分岐を増やさない対策
条件によって呼ぶクラス(バリエーション)が変わる場合
例えば、条件1の場合はクラスA、条件2の場合はクラスBを呼ぶみたいな感じになる場合です。
インターフェース、抽象クラス、ファクトリクラスを使う。
以下の例であれば、呼ぶ側のクラスである「formClient」というクラスにおいてIFormというインターフェースを呼び出します。ただ、どうしてもどのクラスを呼び出すかの判断は必要になるので、その場合はファクトリクラス(デザインパターンの一つ)を作って呼び出すようにします。
具体的な実装のポイント
インターフェース
ストラテジーパターンになります。
インターフェースを使って同一視させることで、無駄な変数の数を減らすことができます。
同一視できてない場合
1 2 3 4 5 |
if (発注フォームの場合){ const hattyuform:HattyuForm = new HattyuForm(); } else (受注フォームの場合) { const jutyuuform:JutyuForm = new JutyuForm(); } |
同一視できている場合
1 2 3 4 5 6 |
var form:IForm; if (発注フォームの場合){ form = new HattyuForm(); } else (受注フォームの場合) { form = new JutyuForm(); } |
また、クラスの生成自体はファクトリークラスなどにやらせるとソースがスッキリします。
抽象クラス
テンプレートパターンになります。
インターフェースとは異なり、共通的な部分がありつつも一部分だけサブクラスにやらせたい場合などに使えます。
例
「フォームインターフェース」を継承した以下二つのフォームを用意するとします。
- 発注フォーム
- 受注フォーム
「テキストボックス自体をユーザーが削除できる機能」を作りたいとなったとしましょう。その際に、インターフェースを使った状態だと全ての実装クラスに対してその処理を追加しなけばならなくなって保守性が下がってしまいます。そこで「抽象クラス」を使うことによって共通部分を定義したメソッドを定義し、それを継承させるようにすればいちいち全ての実装クラスに対して手入れを行う必要がなくなります。
非推奨:仮想メソッド
具象クラスを継承して作ることになり「抽象クラス/テンプレートパターン」に劣ります。(そもそも、継承は具象クラスに対してしない方が良い。)
ファクトリークラスに関しては以下の記事でまとめています。
レゾルバーを使う。
その場合、普通に実装するとswitch文が発生してしまいます。その対策に使えるのが「レゾルバー」です。
レゾルバーの導入(マッピング)
switchでやっていることを代わりに実行してくれます。
プログラム的に言えば
処理が入力に応じて変わる場合に、入力に対応する処理オブジェクトを返します。(要は、どのクラスを使えば良いかのマッピングの仕組みになります。)
具体的には
- 各インスタンスにマッピングの処理を持たせます。(supportメソッドなど)
- resolverクラスを別途作り、メソッドを登録させて、supportを呼ぶ作りにする。
全てのクラス(バリエーション)を呼ぶ場合
全てのクラスをループ処理で実行するので、オブジェクト指向のポリモーフィズムが使えます。
この記事へのコメントはありません。