2021

7

11

オブジェクト指向を復習してやんよ!!!(SOLID原則編)

タグ: |

Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

オブジェクト指向の理解がなんとなくぼんやりしていたので、良書と言われる「オブジェクト指向設計ガイド」をインプット復習してみる。

今回は1、2章のSOLID原則をメインに本書はRubyですが書き慣れたPythonで理解していきます

SOLID原則とは

  • S・・・Single Responsibility Principle(単一責任の原則)
  • O・・・open/closed principle(開放閉鎖の原則)
  • L・・・Liskov substitution principle(リスコフの置換原則)
  • I・・・Interface segregation principle(インターフェース分離の原則)
  • D・・・dependency inversion principle(依存性逆転の原則)

頭文字それぞれの原則をまとめたもの


①単一責任の原則 (Single Responsibility Principle)

1つのクラスは1つだけの責任を持たなければならない。

複数の責任は他の責任への変更をもたらしてしまうので、クラスは1つのことだけ責任を負うべき適切に分割しましょうの意

原則に沿わない例

class Account:
   """ 銀行口座クラス """
   def __init__(self, account_no: str):
       self.account_no = account_no

   def get_account_number(self):
       """アカウント番号を取得"""
				return self.account_no

   def save(self):
				"""アカウント番号をDBに保存"""
		    pass

この例だと銀行口座クラスが「アカウント取得」と「データ保存」の2つのタスクを実行させているが、
「利用者の口座所持の証明」や「口座データのコピー・移行」など
細かい機能を追加していくほどにこのクラス自身が抱える責任がより大きく複雑になり技術的負債の温床になる

原則に沿った例

class AccountDB:
   """ DBアカウント管理 """

   def get_account_number(self, _id):
       """アカウント番号取得"""
       pass

   def account_save(self, obj):
       """アカウント番号を登録"""
       pass


class Account:
   """ 銀行口座クラス """

   def __init__(self, account_no: str):
       self.account_no = account_no
       self._db = AccountDB()

   def get_account_number(self):
       """口座番号取得"""
       return self.account_no

   def get(self, _id):
       return self._db.get_account_number(_id=_id)

   def save(self):
       """口座番号を登録"""
       self._db.account_save(obj=self)
  • データベースのジョブと口座管理のジョブを分けることで、より責任の所在が明確になる
    • DBアカウント管理クラス ⇒ 口座へのデータの取得or保存だけを担う
    • 銀行口座クラス ⇒ 利用者自身の口座所持の証明及び手続きできる権利だけを担う

役割が抽象的な機能追加の際に本書の「コードの書き方は知っているけど、どこに置けばよいのかわからない」の部分の判断材料になると思われる


②開放閉鎖の原則(open/closed principle)

拡張に対して開いて (open) いなければならず、修正に対して閉じて (closed) いなければならない

既存のコードを変更せずに新しいモジュールを追加できるように開発する必要がある

原則に沿わない例

# 指定の参照ファイルから単語出現回数カウント
def count_word_occurrences(word, localfile):
	content = return open(file, "r").read()
	counter = 0
	for e in content.split():
		if word.lower() == e.lower():
		counter += 1
	return counter

print(count_word_occurrences("abc", test.txt)) # --> 3
  • これに単語の毎の出現比率を取りたい機能追加の変更時に生まれるデメリット
    • 取得したファイル情報&分割した単語を再利用できない
    • 最終的に返る値が数字のみなのでメソッド自体の汎用性が乏しい

原則に沿った例

# 指定ファイルの読み込み
def read_localfile(file):
	return open(file, "r").read()

# ソース内の単語単位で分割
def number_of_words(content):
    return len(content.split())

# ソース内の単語数をカウント
def count_word_occurrences(word, content):
    counter = 0
    for e in content.split():
        if word.lower() == e.lower():
            counter += 1
    return counter

# 分割された単語ごとの出現比率
def percentage_of_word(word, content):
    total_words = number_of_words(content)
    word_occurrences = count_word_occurrences(word, content)
    return word_occurrences/total_words

「ファイル読み込み」「分割」「カウント」の役割を事前に分けることで間に別工程が入っても(open)、既存の3つの役割に手を加える必要がない(close)設計になる


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

派生元の型(基底型)と派生先の型(派生型)の間に成り立っていなければならない規則性

  • 派生元・・・オブジェクトやクラスやインターフェースなどの仕様書的な役割
  • 派生先・・・継承やサブクラスなど派生元からなる実装手段

原則に沿わない例

class Vehicle:
   """車両情報サブクラス"""

   def __init__(self, name: str, speed: float):
       self.name = name
       self.speed = speed

   def get_name(self) -> str:
       """車両名"""
       return f"車両名: {self.name}"

   def get_speed(self) -> str:
       """車両速度"""
       return f"速度: {self.speed}"

   def engine(self):
       """エンジン"""
       pass

   def start_engine(self):
       """エンジンスタート"""
       self.engine()


class Car(Vehicle):
   """自動車クラス"""
   def start_engine(self):
       pass


class Bicycle(Vehicle):
   """自転車クラス"""
   def start_engine(self):
       pass

自動車クラスと自転車クラスにそれぞれ車両クラスのサブクラスのエンジンスタートの機能を実行させると、当たり前だが自転車にはエンジンが搭載されていない(コード上は省略)ので実行できない

原則に沿った例

class Vehicle:
   """車両管理サブクラス"""
   def __init__(self, name: str, speed: float):
       self.name = name
       self.speed = speed

   def get_name(self) -> str:
       """車両名"""
       return f"車両名: {self.name}"

   def get_speed(self) -> str:
       """車両スピード"""
       return f"速度: {self.speed}"

class VehicleWithoutEngine(Vehicle):
   """エンジン非搭載車両クラス"""
   def start_moving(self):
      """起動"""
      raise "実装されていません"

class VehicleWithEngine(Vehicle):
   """エンジン搭載車両クラス"""
   def engine(self):
      """エンジン"""
      pass

   def start_engine(self):
      """エンジンスタート"""
      self.engine()

class Car(VehicleWithEngine):
   """自動車クラス(エンジン搭載)"""
   def start_engine(self):
       pass

class Bicycle(VehicleWithoutEngine):
   """自転車クラス(エンジン非搭載)""" 
   def start_moving(self):
       pass

派生元である基本クラスの自転車&自動車クラスを仕様書としてジョブをスタートさせた場合に
仕様に合ったサブクラスを利用させることで実装できる

wikiによる契約プログラミングという観点

  • 事前条件を派生型で強めることはできない(派生型で上位より強い条件は作れない)
  • 事後条件を派生型で弱めることはできない(派生元で下位より詳細な情報を作れない)
  • 派生型のメソッドが発生する例外は、上位の型のメソッドが発生する例外の派生型 or 上位のメソッドの例外と同じものでなければならない
  • サブクラスの入力のパターンが基本クラスの入力のパターンよりも厳しい
    • 利用する側はサブクラスを利用する際にサブクラスが許容する入力であるかどうかを意識する必要が出てきてしまう
  • サブクラスの出力のパターンが基本クラスの出力のパターンよりも緩い
    • 利用する側はサブクラスを利用する際にサブクラスが予期せぬ出力をしないか意識する必要が出てきてしまう

階層型のif文書いた事ある人なら無意識で身についてる部分だと思うが、
親から子に処理が移譲されていく流れの中で、バグが発生しない様な自然な摂理をちゃんとコードにも実装しましょうという認識をしました。


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

クライアントが使用しないメソッドに依存することを強制されるべきではない

汎用的な目的のインターフェイスが1つだけあるよりも、特定のクライアント向けのインターフェイスが多数あった方がよりよい

原則に沿わない例

class Shape:
   """ 変形クラス """
   def draw_circle(self):
       """ 円を描画 """
       raise "実装されていません"
   def draw_square(self):
       """ 正方形を描画 """
       raise "実装されていません"

class Circle(Shape):
    """ 円クラス """
   def draw_circle(self):
       """ 円を描画 """
       pass
   def draw_square(self):
       """ 正方形を描画 """
       pass

見たとおり、ユーザーが円を必要としている時点で円クラスに原型が異なる正方形を描画する不要な機能を持たせてしまうとインターフェースの入り口となるCircleクラス自体が曖昧になってしまう

原則に沿った例

class Shape:
   """ 変形クラス """
   def draw(self):
      """成形"""
      raise "実装されていません"

class Circle(Shape):
    """ 円クラス """
   def draw(self):
      """ 円を描画 """
      pass

class Square(Shape):
    """ 正方形クラス """
   def draw(self):
      """ 正方形を描画 """
      pass

便利さを追求するあまり、ユーザーが本当に欲しかったモノ論や、搭載した機能の複雑さが単一責任の原則違反にもつながってくる部分なので、本来の意図に沿ったジョブを適したクラスに分離させましょうなヤツ


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

ソフトウェアの振る舞いを定義する上位レベルのモジュールから下位レベルモジュールへの従来の依存関係は逆転し、結果として下位レベルモジュールの実装の詳細から上位レベルモジュールを独立に保つことができるようになる

設計上望ましい依存の方向性と、素直に実装しようとしたときの方向性は矛盾しちゃうので、そこをテクニックでカバーして逆転させると、じつはスッキリと望ましい設計通りに実装できるという意

原則に沿わない例

class BackendDeveloper:
    """ バックエンド設定(下位モジュール) """
    @staticmethod
    def python():
        print("Pythonコードの記述")

class FrontendDeveloper:
    """ フロントエンド設定(下位モジュール) """
    @staticmethod
    def javascript():
        print("JavaScriptコードの記述")

class Project:
   """ プロジェクト構成(上位モジュール) """
    def __init__(self):
        self.backend = BackendDeveloper()
        self.frontend = FrontendDeveloper()

    def develop(self):
        self.backend.python()
        self.frontend.javascript()
        return "開発言語"

project = Project()
print(project.develop())

pythonでは @staticmethod デコレータを利用することで、インスタンス化(__init__)されてないクラスの他の実装に依存しない独立した関数として実装できる

上記は上位モジュールのProjectクラスが下位モジュールのメソッド依存している状態。 選択言語となる下位モジュールが変更される度に上位クラスのProject内のdevelopメソッド内の実行部分のself.backend.python()self.frontend.javascript()にも修正が必要になり、依存逆転に反してしまう

原則に沿った例

class BackendDeveloper:
    """ バックエンド設定(下位モジュール) """
   def develop(self):
       self.__python_code()

   @staticmethod
   def __python_code():
       print("Writing Python code")

class FrontendDeveloper:
    """ フロントエンド設定(下位モジュール) """
   def develop(self):
       self.__javascript()

   @staticmethod
   def __javascript():
       print("JavaScriptコードの記述")

class Developers:
   """開発環境クラス(抽象モジュール)"""
   def __init__(self):
       self.backend = BackendDeveloper()
       self.frontend = FrontendDeveloper()

   def develop(self):
       self.backend.develop()
       self.frontend.develop()

class Project:
   """ プロジェクト構成(上位モジュール) """
   def __init__(self):
       self.__developers = Developers()
   def develops(self):
       return self.__developers.develop()

project = Project()
print(project.develops())
  • Developersクラスという抽象モジュールを差し込むことでインスタンス宣言に指定したフロント&バックのdevelopメソッドから直接実行結果のみを定義できるようになる
  • これにより下位モジュールが上位モジュールのProjectクラスには直接依存せずに下位モジュールの特定メソッドの実行結果を得れる

まとめ

  • 単一責任の原則(SRP)
    • 複数の責任を押し付けるとキャパオーバーしちゃうので単一の担当を心がけよう
  • 開放閉鎖の原則(OCP)
    • 既存のコードに破壊的変更を加えると広範囲に影響が出ちゃうので拡張していこう
  • リスコフの置換原則(LSP)
    • 結果を理解していないとインターフェースには意味が無いのでそういうのはやめよう
  • インターフェース分離の原則(ISP)
    • 役割が異なるものは分離させてインターフェースの機能は最小限にしよう
  • 依存性逆転の原則(DIP)
    • 実体同士の依存関係をどんどん繋いでいくと破綻するので、スキルやデザインパターンなりライブラリを利用して適度に関係性を抽象化させよう
Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

トップへ