TL:DR
時間がない人用に、本記事のまとめを以下に記載します。
- SOLID原則は、変更に強く、保守しやすいソフトウェアを設計するための5原則
- SOLID原則は、変更箇所を局所化することで、仕様変更・機能追加のコストを下げ素早い開発を可能にする。開発者が気持ちよくなりたくてSOLID原則に当てはめてコード修正するものではない。
| 頭文字 | ざっくり一言 |
|---|---|
| S(単一責任の原則) | 変更理由が複数出そうなでかいクラス・モジュールを作るな!! |
| O(開放・閉鎖の原則) | 既存コードを変えずに、追加のみで機能拡張できるようにしろ!! |
| L(リスコフの置換原則) | 親クラスで実装されてるなら子クラスでも使えるようにしろ!! |
| I(インターフェース分離の原則) | 使わないメソッドの実装を強制するでかいインターフェースを作らず分割して関連があるものだけ作れ!! |
| D(依存性逆転の原則) | 具体的な実装でなく抽象クラスに依存しろ!! |
はじめに
メンター業務を実施しているときに、SOLID原則の話になりS・Oは説明できたのですが、
それ以降を忘れていたので備忘として記事にしました。
個人的に会社に例えると分かりやすかったので、本記事では会社に例えて説明しています。
(ちょっとフックが効くタイトルにしてますがご愛嬌ということで許してください!!)
SOLID原則とは?
SOLID原則はWikiによると、
アメリカのソフトウェア技術者ロバート・C・マーティン(英語版)が提唱していた数々の設計原則の中からチョイスされたものである。マーティンが提唱していた原則は、例えば2000年に発表されたレポート『Design Principles and Design Patterns』[1]の中で紹介されている[2][3][4]。そのうち五原則をSOLIDという語呂合わせの頭字語にして普及させたのは、ソフトウェア技術者マイケル・フェザーズであり、2004年以降に広く知られるようになった[5]。SOLIDはオブジェクト指向設計由来であるが、アジャイルソフトウェア開発や適応的ソフトウェア開発(英語版)といった方法論の哲学にもなっている。
と記されています。
要するに、設計するときに意識しといたら良いこと5選 です。(個人的解釈)
なぜSOLID原則が必要か?
SOLID原則だけでなく、なぜ設計が必要かは他の記事でも記載がありますが、
個人的には、以下を実現するために必要であると考えています。
- 素早い機能開発を実現し、競争優位性を高めるため
- デグレの発生を抑え、機能追加によるバグの発生を少なくするため
- 障害が発生した際の影響範囲特定を迅速に行えるようにするため
- 新規参画者の認知負荷を少なくし、即戦力化しやすくするため(チームの生産性向上)
はい。あまり自分の考えを書くのはちょっと恥ずかしいので、
冒頭でも記載したとおり、会社に例えて一つずつ見ていきましょう。
S(単一責任の原則)
ある、会社があるとします。
経理部などの部門があるが、以下のようになっていたとします。
class 社長:
def 会社を管理する(self):
pass
def 営業する(self):
pass
def 経費を精算する(self):
pass
def 人を採用する(self):
pass
上記では、例えば経費ルールが変わってしまっただけで、様々な処理を抱えた社長クラスを変更することになってしまいます。
こういった多機能なクラスは大抵、多くの理由で変更されるため、変更時の影響範囲やレビュー・テスト範囲が広がります。
そのため、以下のようにして変更箇所を最小限にしましょうという考えが単一責任の原則です。
class 社長:
def 会社を管理する(self):
pass
class 営業部:
def 営業する(self):
pass
class 経理部:
def 経費を精算する(self):
pass
class 人事部:
def 人を採用する(self):
pass
上記のようにすることで、役割分担が明確になり、影響範囲を小さくすることができるようになりました。
O(開放・閉鎖の原則)
SOLID原則2つめ、開放・閉鎖の原則を説明します。
営業部に注目してみましょう。
営業部は法人営業・個人営業を主にやっていたとします。
実コードに落とすと以下のような感じです。
class 営業部:
def 営業する(self):
if 営業種別 == "法人営業":
pass
if 営業種別 == "個人営業":
pass
会社が順調に成長した場合に、海外営業が追加となった場合どうなるでしょうか?
はい。そうですね。Sでも述べたように、def 営業するに追加するしかなく、既存メソッドに影響が出てしまいました。
なので、以下のようにして変更箇所を最小限にしましょうという考えが開放・閉鎖の原則です。
class 営業部(ABC):
@abstractmethod
def 営業する(self):
pass
class 法人営業部(営業部):
def 営業する(self):
print("法人へ営業します")
class 個人営業部(営業部):
def 営業する(self):
print("個人へ営業します")
class 海外営業部(営業部):
def 営業する(self):
print("海外の顧客へ営業します")
if __name__ == "main":
print(営業部.営業する)
このようにすることで営業部の営業するを変更することなく、クラス追加だけで営業方法を増やすことができるようになりました。
L(リスコフの置換原則)
SOLID原則3つめ、リスコフの置換原則を説明します。
続いては人事部に着目してみましょう。
人事部は新卒採用・中途採用のみを行っていたとします。
コードにすると以下です。
class 人事部(ABC):
@abstractmethod
def 採用する(self):
pass
class 中途採用担当者(人事部):
def 採用する(self):
pass
class 新卒採用担当者(人事部):
def 採用する(self):
pass
def 採用を実行する(担当者):
担当者.採用する()
if __name__ == "main":
採用を実行する(中途採用担当者())
def 採用を実行するは担当者が中途であろうと新卒であろうと、意識せずに動きます。
ここで、元人事部が加わったとします。
class 人事部(ABC):
@abstractmethod
def 採用する(self):
pass
class 中途採用担当者(人事部):
def 採用する(self):
pass
class 新卒採用担当者(人事部):
def 採用する(self):
pass
class 元人事部(人事部):
def 採用する(self):
raise ValueError("部外者のため採用不可")
def 採用を実行する(担当者):
担当者.採用する()
上記の場合に採用を実行するに元人事部が渡された場合にエラーとなってしまいます。
使う側がわざわざ、エラーとならないように元の実装を確認しにいくのは無駄ですよね?
そのため、親クラスとして扱える場所では、どの子クラスに差し替えても呼び出し側の期待どおりに動くようにするという考え方が、リスコフの置換原則です。
I(インターフェース分離の原則)
SOLID原則4つめ、インターフェース分離の原則を説明します。
経理部門に注目してみましょう。
経理部門はやることが多く以下のように多忙だったとします。
class 経理部(ABC):
@abstractmethod
def 経費を精算する(self):
pass
@abstractmethod
def 請求書を発行する(self):
pass
@abstractmethod
def 売り上げ分析(self):
pass
@abstractmethod
def 決算資料の作成をする(self):
pass
@abstractmethod
def 給与を計算する(self):
pass
@abstractmethod
def 市場調査(self):
pass
経理部を引き継いだ経理担当者を作るとします。
市場調査など経理とは関係ないものを実装するのは大変ですよね?
また、経理部とは関係がなさそうな、市場調査メソッドに変更が入った際にこのクラスを引き継いだ経理担当などの子クラスに影響が発生してしましまいます。(余分な依存関係をつくってしまっています。)
そのため、経営戦略部を新しく作って分けてみるとどうでしょうか?
class 経理部(ABC):
@abstractmethod
def 経費を精算する(self):
pass
@abstractmethod
def 請求書を発行する(self):
pass
@abstractmethod
def 給与を計算する(self):
pass
class 経営戦略部(ABC):
@abstractmethod
def 売り上げ分析(self):
pass
@abstractmethod
def 決算資料の作成をする(self):
pass
@abstractmethod
def 市場調査(self):
pass
これで、経理担当では使わない市場調査メソッドの実装を避けることができるようになりました。
また、市場調査メソッドに変更が入った場合は、経営戦略部の子供には影響が発生してしまいますが、経理部の子供には影響をおさえることができました。
使う側に、使わないメソッドへの依存や実装を強制しないよう、用途ごとに小さなインターフェースへ分割する。
これがインターフェース分離の原則です。
D(依存性逆転の原則)
SOLID原則5つめ、依存性逆転の原則について説明します。
社長は秘書の佐藤さんにスケジュール管理を任せていたとします。
class 社長:
def 会社を管理する(self):
pass
def スケジュールを管理する(self):
佐藤さん.スケジュールを管理する
class 佐藤さん:
def スケジュールを管理する(self):
pass
上記では、社長クラスが佐藤さんクラスに依存してしまっています。
そのため、佐藤さんクラスのスケジュールを管理するが変更になった場合、社長クラスにも影響してしまいます。
以下のように社長のスケジュール管理は秘書がやるというように変更してみるとどうでしょうか?
class 社長:
def __init__(self, 秘書):
self.秘書 = 秘書
def 会社を管理する(self):
pass
def スケジュールを管理する(self):
self.秘書.スケジュールを管理する
class 秘書(ABC):
@abstractmethod
def スケジュールを管理する(self):
pass
class 佐藤さん(秘書):
def スケジュールを管理する(self):
pass
社長クラスは、スケジュールを管理する秘書クラス(抽象)に依存するようになりました。
このようにすることで、佐藤さんクラスのスケジュールを管理するメソッドに依存していた社長クラスが、
秘書クラスの約束事を守ってくれて入れば何でもよくなりました。(秘書クラスを継承した鈴木さんクラスでもよいため、すぐに付け替えられる)
この具体的な実装に依存させるのではなく、抽象クラスに依存させようという考え方が、依存性逆転の原則です。
以上5つになります。
お疲れさまでした。
最後に
ここまで読んでいただきありがとうございました。
設計の話は説明が難しく and 私が説明下手なこともあり読みづらい部分も多分にあったかと思います(ごめんなさい)
設計などの概念的な話は個人的にすぐ忘れてしまったり、なんとなくで理解していたので今回、再度調べてみて腑に落ちることや勉強になった事が多かったと感じています。
この記事が誰かの役に立てば幸いです。ありがとうございました。