プログラミングでコードを記述するにあたり、SOLIDの原則を守ることで可読性・保守性・拡張性が向上します。
SOLIDとは、各原則の英語表記の頭文字をとったものです。
- 原則1(S)単一責任の原則(Single responsibility principle)
- 原則2(O)解放閉鎖の原則(Open/Closed principle)
- 原則3(L)リスコフの置換原則(Liskov substitution principle)
- 原則4(I)インターフェース分離の原則(Interface segregation principle)
- 原則5(D)依存性逆転の原則(Dependency inversion principle)
私自身、この原則を守ったコードはまだ記述することができません。しかし、コードを記述するにあたり、常にこの原則を思い出しながら、少しでも近づけるような努力をしていきたいと思います。
原則1(S)単一責任の原則(Single responsibility principle)
全てのモジュールとクラスは、一つの役割を提供して責任をもつべきとする原則です。
pytyonによるサンプルコード
# single_responsibility.py
class UserInfo(object):
"""ユーザ情報を保持する"""
def __init__(self, name, age, phone_number):
self.name = name
self.age = age
self.phone_number = phone_number
def __str__(self):
return "{} {} {}".format(
self.name, self.age, self.phone_number
)
class FileManager(object):
@staticmethod
def write_str_to_file(obj, filename):
with open(filename, mode='w') as fh:
fh.write(str(obj))
def main():
user_info = UserInfo('Taro', 21, '000-000-0000')
print(user_info)
#write to file. インスタンスを作る必要はない。
FileManager.write_str_to_file(user_info, 'temp.txt')
if __name__ == '__main__':
main()
原則2(O)解放閉鎖の原則(Open/Closed principle)
クラス、モジュール、関数などのソフトウェアのぶひんは拡張に対しては開いており、修正に対しては閉じていなければならないとする原則です。
pytyonによるサンプルコード
# 開放閉鎖の原則1
# open_closed.py
# 開放閉鎖の原則を守った記述するには、、
from abc import ABCMeta, abstractmethod
class UserInfo(object):
"""ユーザ情報の保持"""
def __init__(self, user_name, job_name, nationality):
self.user_name = user_name
self.job_name = job_name
self.nationality = nationality
def __str__(self):
return "{} {} {}".format(
self.user_name, self.job_name, self.nationality
)
"""開放閉鎖の原則を守った作り方
以下のように定義すると、機能拡張をしたい場合は継承クラスによって行うことができる。
つまり、作成ずみのクラスを編集して汚す必要がなくなる。
"""
class Comparation(metaclass=ABCMeta):
@abstractmethod
def is_equal(self, other):
pass
# 応用編
def __and__(self, other):
return AndComparation(self, other)
def __or__(self, other):
return OrComparation(self, other)
class AndComparation(Comparation):
def __init__(self, *args):
# 可変長の引数をとる
self.comparations = args
def is_equal(self, other):
return all(
map(
lambda comparation: comparation.is_equal(other),
self.comparations
)
)
class OrComparation(Comparation):
def __init__(self, *args):
# 可変長の引数をとる
self.comparations = args
def is_equal(self, other):
return any(
map(
lambda comparation: comparation.is_equal(other),
self.comparations
)
)
class Filter(metaclass=ABCMeta):
@abstractmethod
def filter(self, comparation, item):
pass
class JobNameComparation(Comparation):
def __init__(self, job_name):
self.job_name = job_name
def is_equal(self, other):
return self.job_name == other.job_name
class NationalityComparation(Comparation):
def __init__(self, nationality):
self.nationality = nationality
def is_equal(self, other):
return self.nationality == other.nationality
class UserInfoFilter(Filter):
def filter(self, comparation, items):
for item in items:
if comparation.is_equal(item):
yield item
# # 開放閉鎖の原則を破ったクラス
# # 何か新しい検索をするために、このクラスを編集して新たなメソッドを追加したりする必要がある。
# class UserInfoFilter(object):
# """ユーザを検索する"""
# @staticmethod
# def filter_users_job(users, job_name):
# for user in users:
# if user.job_name == job_name:
# yield user
#
# @staticmethod
# def filter_users_nationality(users, nationality):
# for user in users:
# if user.nationality == nationality:
# yield user
taro = UserInfo('taro', 'salary man', 'Japan')
jiro = UserInfo('jiro', 'police man', 'Japan')
john = UserInfo('john', 'salary man', 'USA')
user_list = [taro, jiro, john]
salary_man_comparation = JobNameComparation('salary man')
user_info_filter = UserInfoFilter()
print(' --- job name --- ')
for user in user_info_filter.filter(salary_man_comparation, user_list):
print(user)
print(' --- nationarity --- ')
japan_comparation = NationalityComparation('Japan')
for user in user_info_filter.filter(japan_comparation, user_list):
print(user)
#AndComparationを使ってみる。 & をつかつ
print('-'*10)
salary_man_and_japan = salary_man_comparation & japan_comparation
for user in user_info_filter.filter(salary_man_and_japan, user_list):
print(user)
#OrComparationを使ってみる。| (パイプ)でつなぐ
print('-'*10)
salary_man_or_japan = salary_man_comparation | japan_comparation
for user in user_info_filter.filter(salary_man_or_japan, user_list):
print(user)
# for man in UserInfoFilter.filter_users_job(user_list, 'police man'):
# print(man)
#
# for man in UserInfoFilter.filter_users_nationality(user_list, 'Japan'):
# print(man)
原則3(L)リスコフの置換原則(Liskov substitution principle)
サブクラスは、そのスーパークラスの代用ができなければならないとする原則です。
(スーパークラスが使えるものは、全てサブクラスでも使えるようにします。)
(目的)
・スーパークラスの仕様を理解すれば、それを継承したサブクラスは
中身を全てを確認しなくても利用することができる。(拡張性、保守性の向上)
>サブクラスとスーパークラスの間で差異(実行できるものとできないもの)があると
サブクラスを使うために、サブクラスを全て理解する必要がでてしまう。
(例)
関数φ(x):
Tクラスのインスタンスx で実行できる場合、
Tクラスのサブクラスのインスタンスy でも実行できること
pythonによるサンプルコード
# liskov_substitution.py
"""
スーパクラスで使えるものは、サブクラスでも使えるようにしておく
"""
class Rectangle(object):
""" 長方形 """
def __init__(self, width, height):
self._width = width
self._height = height
# propertyとgetter, setterメソッドはセットで考える
# https://naruport.com/blog/2019/8/27/python-tutorial-class-property-getter-setter/
@property
def width(self):
return self._width
@width.setter
def width(self, width):
self._width = width
@property
def height(self, height):
self._height = height
@height.setter
def height(self, height):
self._height = height
def calcurate_area(self):
return self._width * self._height
class Square(Rectangle):
def __init__(self, size):
self._width = self._height = size
"""親クラスのセッターをオーバライドする(プロパティの再定義がいらない)"
また、pythonの使用上、getterも定義しなければならなくなるとのこと
"""
@Rectangle.width.setter
def width(self, size):
self._width = self._height = size
@Rectangle.height.setter
def height(self, size):
self._width = self._height = size
def print_area(obj):
change_to_width = 10
change_to_height = 20
# このままではリスコフの置換原則を満たしていない
# そのため、Squareが入ってきているのに、10*20をしてしまう。
obj.width = change_to_width
obj.height = change_to_height
# 修正方法1(Squareの場合、強引に値を修正する)
if isinstance(obj, Square):
# このプログラム内で、squareオブジェクトに合わせる
change_to_width = change_to_height
# 修正方法2(考え方のみ)他のやり方としては、Reactangleの継承ではなく、もっと抽象的なクラスを作成して
# print_areaはSquareで実行できないようにする、という方法も考えられる。
print('Predicted Area = {}, Actual Area = {}'.format(
change_to_height * change_to_width,
obj.calcurate_area()
))
if __name__ == '__main__':
rc = Rectangle(2, 3)
print_area(rc)
sq = Square(5)
print_area(sq)
原則4(I)インターフェース分離の原則(Interface segregation principle)
インターフェース(抽象クラス)のメソッドが多すぎる場合は、インターフェースを分離します。
(目的)
・インターフェース上に必要ないメソッドを追加して、継承先で無駄なコードを作成することがないようにする。
*インターフェースを継承した場合、定義されているメソッドは必ずオーバライドして
実際のコードを書かないといけないので、使わない場合はただの無駄なコード。
・継承するインターフェースのもつメソッドは必要最小限とする。
(インターフェースのもつメソッドが増えすぎると、コードが複雑になり理解が難しくなる)
・インターフェースの継承先が増えすぎて、インターフェースの修正による影響範囲が大きくなり保守性が低下する
*インターフェースとは
メソッドの中身を記載していない継承して利用するためのクラス
pythonによるサンプルコード
# interface_segregatioin.py
from abc import ABCMeta, abstractmethod
# 修正コード
class Athlete(metaclass=ABCMeta):
pass
"""以下のようにインターフェースを分ける"""
class SwimAthlete(Athlete):
@abstractmethod
def swim(self):
pass
class JumpAthlete(Athlete):
@abstractmethod
def high_jump(self):
pass
@abstractmethod
def long_jump(self):
pass
# インターフェースを継承したクラスを作成
class Athlete1(SwimAthlete):
def swim(self):
print('I swim')
# 多重継承によって、複数のインタフェースを継承することも可能
class Athlete2(SwimAthlete, JumpAthlete):
def swim(self):
print('I swim')
def high_jump(self):
print('I jump high')
def long_jump(self):
print(' i jump long')
if __name__ == '__main__':
john = Athlete1()
john.swim()
yuji = Athlete2()
yuji.high_jump()
原則5(D)依存性逆転の原則(Dependency inversion principle)
高水準なモジュールは、低水準なモジュールに依存してはいけません。両者は抽象化に依存すべきとする原則です。
依存性逆転の原則を守ることで、低水準のモジュールを継承したクラスを利用した機能拡張が容易になります。
(原則)
・高水準なモジュールは、低水準のモジュールに依存してはいけない。
両者は抽象化に依存すべき。
・抽象化は詳細に依存すべきではなく、詳細は抽象化に依存すべき。
(例)
class A(metaclass=ABCMeta):
pass
class B(A):
pass
このようにクラスBがクラスAを継承している場合、
例えば新たなクラスCは、クラスBではなく、クラスAを継承するようにする。
class C(A):
pass
pythonによるサンプルコード
# dependency_inversion.py
# 修正コード
from abc import ABCMeta, abstractmethod, abstractproperty
class IBook(metaclass=ABCMeta):
"""
インターフェースとなる抽象クラスを定義
頭文字に'I'をつけて表すことが多い
"""
@abstractproperty
def content(self):
pass
class Book(IBook):
"""IBookを継承した、詳細クラスの作成"""
def __init__(self, content):
self._content = content
@property
def content(self):
return self._content
class EBook(IBook):
"""IBook を継承して、別の詳細クラスを定義する
これにより、EBookも、IBookを前提として作成されている様々なクラスが
そのまま活用できることになる。
"""
def __init__(self, content):
self._content = content
@property
def content(self):
return 'E-' + self._content
class IFormatter(metaclass=ABCMeta):
""" 抽象クラスIBookを引数とする、抽象クラスのIFormatterを定義する"""
@abstractmethod
def format(self, i_book: IBook):
pass
class HtmlFormatter(IFormatter):
""" IFormatterを継承した詳細クラスの作成"""
def format(self, i_book: IBook):
return '<h1>' + i_book.content + '</h1>'
class XMLFormatter(IFormatter):
"""このようにすることで、
新しくXMLのフォーマッターを作成する場合に継承できる
"""
def format(self, i_book: IBook):
return '<xml/>' + i_book.content + '</xml>'
class Printer(object):
""" 引数とするクラスは、常に抽象クラスとする(詳細クラスにしない)"""
def __init__(self, i_formatter: IFormatter):
self.i_formatter = i_formatter
def print(self, i_book: IBook):
formatted_book = self.i_formatter.format(i_book)
print(formatted_book)
if __name__ == '__main__':
book = Book('My Book')
html_formatter = HtmlFormatter()
html_printer = Printer(html_formatter)
html_printer.print(book)
xml_formatter = XMLFormatter()
xml_printer = Printer(xml_formatter)
xml_printer.print(book)
ebook = EBook('My EBook')
# プリンターを操作せずに活用ができる!
html_printer.print(ebook)