nono blog
楽しい人生を送りたい!
本紹介

SOLIDの原則(プログラミング)

プログラミングでコードを記述するにあたり、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)