SOLID原則:クリーンで保守しやすいコードのためのオブジェクト指向設計マスター
SOLIDは、オブジェクト指向プログラミングにおけるクラス設計のためのニーモニック(記憶補助)頭字語です。これらの原則は、良いプログラミング習慣と保守しやすいコードを開発するためのプラクティスを規定しています。
長期的な視点でコードの保守性と拡張性を考慮することで、SOLID原則はアジャイルなコード開発環境を豊かにします。コード依存関係を把握し最適化することで、よりシンプルで整理されたソフトウェア開発ライフサイクルを創出します。

SOLID原則とは
SOLIDは、クラス設計のための一連の原則を表します。ロバート・C・マーティン(アンクル・ボブ)がほとんどの設計原則を導入し、頭字語を考案しました。SOLIDは以下の5原則の頭文字をとったものです。
- Single-Responsibility Principle(単一責任の原則)
- Open-Closed Principle(開放閉鎖の原則)
- Liskov Substitution Principle(リスコフの置換原則)
- Interface Segregation Principle(インターフェース分離の原則)
- Dependency Inversion Principle(依存性逆転の原則)
SOLID原則は、ソフトウェア設計のためのベストプラクティス集合体です。各アイデアは設計フレームワークを表し、より良いプログラミング習慣、改善されたコード設計、より少ないエラーへと導きます。
SOLID:5つの原則を解説
SOLID原則の仕組みを理解する最良の方法は例を通じてです。すべての原則は相補的で、個別のユースケースに適用されます。原則を適用する順序は重要ではなく、すべての原則があらゆる状況で適用可能というわけではありません。

以下の各セクションでは、Pythonプログラミング言語で各SOLID原則の概要を提供します。SOLIDの一般的な考え方は、PHP、Java、C#など、あらゆるオブジェクト指向言語に適用可能です。ルールを一般化することで、マイクロサービスなどのモダンなプログラミングアプローチにも適用可能になります。
単一責任の原則(SRP)
単一責任の原則(SRP)はこう述べます:「クラスが変更される理由は、決して1つ以上であってはならない」
クラスを変更する際、単一の機能のみを変更すべきであり、これはすべてのオブジェクトがただ一つの仕事のみを持つべきことを意味します。
以下のクラスを例として見てみましょう:
# 複数の責任を持つクラス
class Animal:
# プロパティコンストラクタ
def __init__(self, name):
self.name = name
# プロパティ表現
def __repr__(self):
return f'Animal(name="{self.name}")'
# データベース管理
def save(animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# プロパティインスタンス化
a = Animal('Cat')
# プロパティをデータベースに保存
Animal.save(a)
save()メソッドに変更を加えるとき、変更はAnimalクラスで発生します。プロパティに変更を加えるときも、変更はAnimalクラスで発生します。
このクラスには変更される理由が2つあり、単一責任の原則に違反しています。コードは期待通りに動作しますが、設計原則を尊重しないことは、長期的にコードの管理を困難にします。
単一責任の原則を実装するには、例のクラスが2つの明確な仕事を持っていることに注目します。
- プロパティ管理(コンストラクタと
get_name()) - データベース管理
(save())

したがって、問題に対処する最善の方法は、データベース管理メソッドを新しいクラスに分離することです。例:
# プロパティ管理を担当するクラス
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
# データベース管理を担当するクラス
class AnimalDB:
def save(self, animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# プロパティインスタンス化
a = Animal('Cat')
# データベースインスタンス化
db = AnimalDB()
# プロパティをデータベースに保存
db.save(a)
単一責任の原則を適用すると、AnimalDBクラスの変更がAnimalクラスに影響を与えなくなります。コードは直感的で修正しやすくなります。
開放閉鎖の原則(OCP)
開放閉鎖の原則(OCP)はこう述べます:「ソフトウェアエンティティは、拡張に対して開いていなければならず、修正に対して閉じていなければならない」
システムに新機能やユースケースを追加する際、既存エンティティの修正を必要としてはなりません。この言葉は矛盾しているように見えます。新機能追加には既存コードの変更が必要だからです。
以下の例で簡単に理解できます:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
class Storage:
def save_to_db(self, animal):
print(f'Saved {animal} to the database')
Storageクラスは、Animalインスタンスの情報をデータベースに保存します。CSVファイルへの保存など、新機能を追加するにはStorageクラスにコードを追加する必要があります:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
class Storage:
def save_to_db(self, animal):
print(f'Saved {animal} to the database')
def save_to_csv(self, animal):
print(f'Saved {animal} to the CSV file')
save_to_csvメソッドは、新機能追加のために既存のStorageクラスを変更しています。このアプローチは、新機能が現れたときに既存要素を変更することで、開放閉鎖の原則に違反します。
コードは、汎用的なStorageクラスを削除し、特定のファイル形式への保存用個別クラスを作成する必要があります。

以下のコードは、開放閉鎖の原則の適用を示しています:
class DB():
def save(self, animal):
print(f'Saved {animal} to the database')
class CSV():
def save(self, animal):
print(f'Saved {animal} to a CSV file')
このコードは開放閉鎖の原則に準拠しています。完全なコードは以下のようになります:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'"{self.name}"'
class DB():
def save(self, animal):
print(f'Saved {animal} to the database')
class CSV():
def save(self, animal):
print(f'Saved {animal} to a CSV file')
if __name__ == '__main__':
a = Animal('Cat')
db = DB()
csv = CSV()
db.save(a)
csv.save(a)
XMLファイルへの保存など、追加機能で拡張しても既存クラスを修正する必要はありません。
リスコフの置換原則(LSP)
リスコフの置換原則(LSP)はこう述べます:「ベースクラスへのポインタや参照を使用する関数は、それを知らずに派生クラスのオブジェクトを使用できなければならない」
この原則は、親クラスが子クラスで置換可能であり、機能に目立った変更がないことを意味します。
以下のファイル書き込み例を見てみましょう:
# 親クラス
class FileHandling():
def write_db(self):
return f'Handling DB'
def write_csv(self):
return f'Handling CSV'
# 子クラス
class WriteDB(FileHandling):
def write_db(self):
return f'Writing to a DB'
def write_csv(self):
return f"Error: Can't write to CSV, wrong file type."
class WriteCSV(FileHandling):
def write_csv(self):
return f'Writing to a CSV file'
def write_db(self):
return f"Error: Can't write to DB, wrong file type."
if __name__ == "__main__":
# 親クラスインスタンス化と関数呼び出し
db = FileHandling()
csv = FileHandling()
print(db.write_db())
print(db.write_csv())
# 子クラスインスタンス化と関数呼び出し
db = WriteDB()
csv = WriteCSV()
print(db.write_db())
print(db.write_csv())
print(csv.write_db())
print(csv.write_csv())
親クラス(FileHandling)は、データベースとCSVファイルへの書き込み用の2つのメソッドで構成されます。クラスは両機能を処理し、メッセージを返します。
2つの子クラス(WriteDBとWriteCSV)は、親クラス(FileHandling)からプロパティを継承します。しかし、両子クラスとも不適切な書き込み関数を使用しようとするとエラーをスローします。これは、オーバーライド関数が親関数に対応していないため、リスコフの置換原則に違反しています。

以下のコードで問題を解決します:
# 親クラス
class FileHandling():
def write(self):
return f'Handling file'
# 子クラス
class WriteDB(FileHandling):
def write(self):
return f'Writing to a DB'
class WriteCSV(FileHandling):
def write(self):
return f'Writing to a CSV file'
if __name__ == "__main__":
# 親クラスインスタンス化と関数呼び出し
db = FileHandling()
csv = FileHandling()
print(db.write())
print(csv.write())
# 子クラスインスタンス化と関数呼び出し
db = WriteDB()
csv = WriteCSV()
print(db.write())
print(csv.write())
子クラスが親関数に正しく対応するようになりました。
インターフェース分離の原則(ISP)
インターフェース分離の原則(ISP)はこう述べます:「一つの汎用インターフェースより、多くのクライアント固有インターフェースの方が良い」
つまり、より大規模なインタラクションインターフェースを、より小さなものに分割します。この原則により、クラスは必要なメソッドのみを使用し、全体的な冗長性を削減します。
以下の例は、汎用インターフェースを示しています:
class Animal():
def walk(self):
pass
def swim(self):
pass
class Cat(Animal):
def walk(self):
print("Struts")
def fly(self):
raise Exception("Cats don't swim")
class Duck(Animal):
def walk(self):
print("Waddles")
def swim(self):
print("Floats")
子クラスは親Animalクラスから継承します。親クラスにはwalkとflyメソッドが含まれます。両関数は特定の動物には適していますが、一部の動物には冗長な機能となります。

この状況を扱うには、インターフェースをより小さなセクションに分割します。例:
class Walk():
def walk(self):
pass
class Swim(Walk):
def swim(self):
pass
class Cat(Walk):
def walk(self):
print("Struts")
class Duck(Swim):
def walk(self):
print("Waddles")
def swim(self):
print("Floats")
FlyクラスはWalkから継承し、適切な子クラスに追加機能を提供します。この例はインターフェース分離の原則を満たします。魚など別の動物を追加するには、魚は歩けないため、インターフェースをさらに原子化する必要があります。
依存性逆転の原則(DIP)
依存性逆転の原則はこう述べます:「具象に依存するのではなく、抽象に依存せよ」
この原則は、抽象化レイヤーを追加することでクラス間の結合を減らすことを目的とします。依存関係を抽象化に移すことで、コードを堅牢にします。
以下の例は、抽象化レイヤーなしのクラス依存を示しています:
class LatinConverter:
def latin(self, name):
print(f'{name} = "Felis catus"')
return "Felis catus"
class Converter:
def start(self):
converter = LatinConverter()
converter.latin('Cat')
if __name__ == '__main__':
converter = Converter()
converter.start()
この例には2つのクラスがあります:
LatinConverter:架空のAPIを使用して動物の学名を取得(簡略化のため「Felis catus」をハードコード)Converter:LatinConverterのインスタンスとその関数を使用して、提供された名前を変換するハイレベルモジュール。ConverterはLatinConverterクラスに大きく依存し、それはAPIに依存しています。このアプローチは原則に違反します。
依存性逆転の原則では、2つのクラスの間に抽象インターフェースレイヤーを追加することが求められます。

解決策の一例は以下の通りです:
from abc import ABC
class NameConverter(ABC):
def convert(self, name):
pass
class LatinConverter(NameConverter):
def convert(self, name):
print('Converting using Latin API')
print(f'{name} = "Felis catus"')
return "Felis catus"
class Converter:
def __init__(self, converter: NameConverter):
self.converter = converter
def start(self):
self.converter.convert('Cat')
if __name__ == '__main__':
latin = LatinConverter()
converter = Converter(latin)
converter.start()
Converterクラスは、LatinConverterに直接依存する代わりに、NameConverterインターフェースに依存するようになりました。将来のアップデートで、NameConverterインターフェースを通じて、異なる言語とAPIを使用した名前変換を定義できるようになります。
なぜSOLID原則が必要なのか
SOLID原則は、デザインパターンの問題と戦うのに役立ちます。SOLID原則の全体的な目標は、コード依存関係を減らすことです。その結果、新機能の追加やコードの一部分の変更が、ビルド全体を壊すことはありません。
オブジェクト指向設計にSOLID原則を適用した結果、コードは理解しやすく、管理しやすく、保守しやすく、変更しやすくなります。ルールが大規模プロジェクトにより適しているため、SOLID原則の適用は全体的な開発ライフサイクルの速度と効率を向上させます。
SOLID原則は今でも有効か?
SOLID原則は20年以上前のものですが、ソフトウェアアーキテクチャ設計の良い基盤を提供し続けています。SOLIDは、オブジェクト指向プログラミングだけでなく、モダンなプログラムと環境にも適用可能な健全な設計原則を提供します。
SOLID原則は、コードが人間によって書かれ修正され、モジュールに整理され、内部または外部の要素を含む状況で適用されます。
結論
SOLID原則は、ソフトウェアアーキテクチャ設計のための良いフレームワークとガイドを提供します。このガイドの例が示す通り、Pythonのような動的型付け言語でも、コード設計に原則を適用することで恩恵を受けられます。
次に、チームがDevOpsを最大限に活用するのに役立つ9つのDevOps原則について読んでみてください。
クラウドコンピューティング
- 摩擦を制御する:ベアリング設計と性能のキープリンシプル
- 高速データ集約型アプリケーション向け堅牢インターコネクトの5つの設計原則
- C# static キーワード:静的変数、メソッド、クラスをマスターする
- C# 入れ子クラスを理解する:定義、使用法、継承
- C++クラステンプレート完全ガイド:実践的なジェネリックプログラミング
- C#の継承:再利用可能で保守しやすいコードを構築する
- C#のポリモーフィズム:静的バインディングと動的バインディングを徹底解説
- Java無名インナークラス完全ガイド:定義、構文、実践例
- Java ObjectOutputStream完全ガイド:シリアル化、メソッド、実践例
- Javaジェネリクス完全ガイド:再利用可能で型安全なコードの構築
- Javaファイル操作のマスター:java.ioによる作成・読み取り・書き込み・削除