オブジェクト指向の理解がなんとなくぼんやりしていたので、良書と言われる「オブジェクト指向設計ガイド」をインプット復習してみる。
今回は1、2章のSOLID原則をメインに本書はRubyですが書き慣れたPythonで理解していきます
頭文字それぞれの原則をまとめたもの
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)
役割が抽象的な機能追加の際に本書の「コードの書き方は知っているけど、どこに置けばよいのかわからない」の部分の判断材料になると思われる
拡張に対して開いて (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)設計になる
派生元の型(基底型)と派生先の型(派生型)の間に成り立っていなければならない規則性
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
派生元である基本クラスの自転車&自動車クラスを仕様書としてジョブをスタートさせた場合に
仕様に合ったサブクラスを利用させることで実装できる
階層型のif文書いた事ある人なら無意識で身についてる部分だと思うが、
親から子に処理が移譲されていく流れの中で、バグが発生しない様な自然な摂理をちゃんとコードにも実装しましょうという認識をしました。
クライアントが使用しないメソッドに依存することを強制されるべきではない
汎用的な目的のインターフェイスが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
便利さを追求するあまり、ユーザーが本当に欲しかったモノ論や、搭載した機能の複雑さが単一責任の原則違反にもつながってくる部分なので、本来の意図に沿ったジョブを適したクラスに分離させましょうなヤツ
ソフトウェアの振る舞いを定義する上位レベルのモジュールから下位レベルモジュールへの従来の依存関係は逆転し、結果として下位レベルモジュールの実装の詳細から上位レベルモジュールを独立に保つことができるようになる
設計上望ましい依存の方向性と、素直に実装しようとしたときの方向性は矛盾しちゃうので、そこをテクニックでカバーして逆転させると、じつはスッキリと望ましい設計通りに実装できるという意
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())