SOLID原則って何ぞや?(開放閉鎖の原則編)

想定読者

  • オブジェクト指向プログラミングに興味がある
  • 可読性が高く、変更に強いプログラムを作りたい
  • SOLID原則を理解して周りに「ドヤァ( ^)o(^ )」したい(自己満足でも可)

はじめに

SOLID原則は以下の5つの原則の頭文字を並べて出来たネーミングです。

単一責任の原則(single-responsibility principle)

There should never be more than one reason for a class to change. 変更するための理由が、一つのクラスに対して一つ以上あってはならない。

開放閉鎖の原則(open/closed principle)←今回のターゲット

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. ソフトウェアの実体(クラス、モジュール、関数など)は、拡張に対して開かれているべきであり、修正に対して閉じていなければならない

リスコフの置換原則(Liskov substitution principle)

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. ある基底クラスへのポインタないし参照を扱っている関数群は、その派生クラスのオブジェクトの詳細を知らなくても扱えるようにしなければならない

インターフェース分離の原則(interface segregation principle)

Many client-specific interfaces are better than one general-purpose interface. 汎用なインターフェースが一つあるよりも、各クライアントに特化したインターフェースがたくさんあった方がよい

依存性逆転の原則(dependency inversion principle)

High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces), [not] concretions. 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも具象ではなく、抽象(インターフェースなど)に依存するべきである

ふむ、なんじゃこりゃって感じですよね。大丈夫です。今から具体例をTypeScriptのコードを用いて解説していきます!

解説は、「悪い例」→「何が悪いか解説」→「良い例」といった順序になります。

開放閉鎖の原則

// 悪い例

// クレジットカードのランクをオブジェクトリテラルで定義
const CreditCardRank {
		NORMAL: 0,
		GOLD: 1,
		PLATINUM: 2,
		BLACK: 3,
} as const;
type CreditCardRank = (typeof CreditCardRank)[keyof typeof CreditCardRank];

// クレジットカードを表現するクラス
class CreditCard {
		constructor(private CreditCardRank rank) {}
}

class Program {
		run1() {
				const creditCard = new CreditCard(CreditCardRank.GOLD);
				if (creditCard.rank !== CreditCardRank.NORMAL) { // クレジットカードのランク判別処理
						// 省略
				}
		}
		run2() {
				const creditCard = new CreditCard(CreditCardRank.PLATINUM);
				if (creditCard.rank !== CreditCardRank.NORMAL) { // クレジットカードのランク判別処理
						// 省略
				}
		}
		run3() {
				const creditCard = new CreditCard(CreditCardRank.BLACK);
				if (creditCard.rank !== CreditCardRank.NORMAL) { // クレジットカードのランク判別処理
						// 省略
				}
		}
}

上記のコードの説明を以下に箇条書きします。

  • CreditCardというクレジットカードを表現したクラス定義
  • CreditCardはrankというクレジットカードのランクを表す属性を持つ
  • Programはアプリケーションの実行処理を記述したクラス(run1, run2, run3という三つの処理が定義されている)
  • run1, run2, run3それぞれにクレジットカードのランク判別処理が記述されている

さて、一体このコードはどこが悪いのでしょうか?「なんかクレジットカードのランク判別処理が重複しているなー」という感想くらいでしょうか?

上記の感想を抱いた読者の方は大変鋭い感覚を持っています。同じようなコードが至る所に表れているのは間違いなく「このソースコードには改善の余地がある」という証です。

ここで仮にクレジットカードのランク名「NORMAL」が「ORDINARY」に変更されたとします。変更されるソースコードの処理内容に注目してください。

// 悪い例(NORMALをORDINARYに変更)

// クレジットカードのランクをオブジェクトリテラルで定義
const CreditCardRank {
		// NORMAL: 0,
		ORDINARY: 0, // 変更後
		GOLD: 1,
		PLATINUM: 2,
		BLACK: 3,
} as const;
type CreditCardRank = (typeof CreditCardRank)[keyof typeof CreditCardRank];

// クレジットカードを表現するクラス
class CreditCard {
		constructor(private CreditCardRank rank) {}
}

class Program {
		run1() {
				const creditCard = new CreditCard(CreditCardRank.GOLD);
				/*
				if (creditCard.rank === CreditCardRank.NORMAL) { // クレジットカードのランク判別処理
						// 省略
				}
				*/
				// 変更後
				if (creditCard.rank !== CreditCardRank.ORDINARY) { // クレジットカードのランク判別処理
						// 省略
				}
		}
		run2() {
				const creditCard = new CreditCard(CreditCardRank.PLATINUM);
				/*
				if (creditCard.rank === CreditCardRank.NORMAL) { // クレジットカードのランク判別処理
						// 省略
				}
				*/
				// 変更後
				if (creditCard.rank !== CreditCardRank.ORDINARY) { // クレジットカードのランク判別処理
						// 省略
				}
		}
		run3() {
				const creditCard = new CreditCard(CreditCardRank.BLACK);
				/*
				if (creditCard.rank === CreditCardRank.NORMAL) { // クレジットカードのランク判別処理
						// 省略
				}
				*/
				// 変更後
				if (creditCard.rank !== CreditCardRank.ORDINARY) { // クレジットカードのランク判別処理
						// 省略
				}
		}
}

上記のソースコードを見ると、変更が入った4つの箇所のうち1つ目は単純に「NORMALからORDINARY」に名称変更したことを定義し直しただけなのでOKです。

問題なのは残りの変更箇所で「全て同じ変更内容」を3つの箇所に施しました。

まさにこれこそが上記のソースコードが悪い例であることの証明になります。「同じような処理がn個所に散りばめられている状態は単純に考えて修正量をn倍に増やす」ことになります。

ここで今回の主題である「開放閉鎖の原則」の定義をもう一度お見せします。

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. ソフトウェアの実体(クラス、モジュール、関数など)は、拡張に対して開かれているべきであり、修正に対して閉じていなければならない

うーん、少しこの定義自体が分かりにくいのでもう少し分かりやすい言葉に書き直してみます。

拡張しやすく、修正しやすいコードにすべき

今回のソースコードは「クレジットカードのランク判別処理のNORMALをORDINARYに変更する」という1つの修正内容に対して、修正箇所が3倍になってしまったので「修正しやすいコードにすべき」という部分に違反していることになりますね。

ということで早速、違反箇所を修正していきましょう!

// 良い例

// クレジットカードを表現するクラス
class NormalCreditCard {}
class GoldCreditCard {}
class PlatinumCreditCard {}
class BlackCreditCard {}

class Program {
		run1() {
				const creditCard = new NormalCreditCard;
		}
		run2() {
				const creditCard = new GoldCreditCard;
		}
		run3() {
				const creditCard = new BlackCreditCard;
		}
}

上記のソースコードは悪い例のように「クレジットカードの種別をCreditCardのrankメンバ変数で管理する」のではなく「クラスごとに分別」するようにしています。

このようにすることで「NORMALからORDINARYへの名称変更」という修正が入っても

// 良い例

// クレジットカードを表現するクラス
// class NormalCreditCard {}
class OrinaryCreditCard {} // 変更後
class GoldCreditCard {}
class PlatinumCreditCard {}
class BlackCreditCard {}

class Program {
    run1() {
		    // creditCard = new NormalCreditCard;
		    const creditCard = new OrinaryCreditCard; // 変更後
    }
    run2() {
		    const creditCard = new GoldCreditCard;
    }
    run3() {
		    const creditCard = new BlackCreditCard;
    }
}

このようなソースコードになり、上記の修正は明らかに「同じような修正内容を複数個所に施す」といった作業から解放されています。

これで「開放閉鎖の原則」についてマスターしたといっても過言ではありません。

是非「私はopen/closed principleを完全に理解した」とドヤってください( ^)o(^ )

次回はSOLIDの「L」の部分である「リスコフの置換原則」について解説します!

良かったら覗いてみてください。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です