サザエさん家族で学ぶSOLID原則入門|Pythonで理解するオブジェクト指向設計の基本

フリーランスボード

20万件以上の案件から、副業に最適なリモート・週3〜の案件を一括検索できるプラットフォーム。プロフィール登録でAIスカウトが自動的にマッチング案件を提案。市場統計や単価相場、エージェントの口コミも無料で閲覧可能なため、本業を続けながら効率的に高単価の副業案件を探せます。フリーランスボード

ITプロパートナーズ

週2〜3日から働ける柔軟な案件が業界トップクラスの豊富さを誇るフリーランスエージェント。エンド直契約のため高単価で、週3日稼働でも十分な報酬を得られます。リモートや時間フレキシブルな案件も多数。スタートアップ・ベンチャー中心で、トレンド技術を使った魅力的な案件が揃っています。専属エージェントが案件紹介から契約交渉までサポート。利用企業2,000社以上の実績。ITプロパートナーズ

Midworks 10,000件以上の案件を保有し、週3日〜・フルリモートなど柔軟な働き方に対応。高単価案件が豊富で、報酬保障制度(60%)や保険料負担(50%)など正社員並みの手厚い福利厚生が特徴。通勤交通費(月3万円)、スキルアップ費用(月1万円)の支給に加え、リロクラブ・freeeが無料利用可能。非公開案件80%以上、支払いサイト20日で安心して稼働できます。Midworks

プログラミングを学んでいると必ず出会う「SOLID原則」。オブジェクト指向設計の基本として知られていますが、抽象的で理解しにくいと感じる方も多いのではないでしょうか。

そこで本記事では、日本人なら誰もが知っている国民的アニメ「サザエさん」の登場人物を例に、SOLID原則をわかりやすく解説します。磯野家やフグ田家の日常を通じて、良いコード設計の考え方を身につけましょう。

SOLID原則とは?

SOLID原則は、保守性が高く拡張しやすいソフトウェアを設計するための5つの設計原則です。アメリカのソフトウェアエンジニア、ロバート・C・マーチン(Uncle Bob)によって提唱されました。

SOLIDは以下の頭文字を取ったものです:

  • S: Single Responsibility Principle(単一責任の原則)
  • O: Open/Closed Principle(開放閉鎖の原則)
  • L: Liskov Substitution Principle(リスコフの置換原則)
  • I: Interface Segregation Principle(インターフェース分離の原則)
  • D: Dependency Inversion Principle(依存性逆転の原則)

それぞれの原則を、サザエさんの世界を例に見ていきましょう。

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

原則の内容

「クラスを変更する理由は1つだけであるべき」という原則です。1つのクラスは1つの責任(役割)だけを持つべきで、複数の責任を持たせるべきではありません。

サザエさんで理解する

磯野家では、サザエさんは主婦として家事全般を担当していますが、もしサザエさんが「料理」「洗濯」「掃除」「買い物」「子育て」「家計簿管理」まで1人のクラスで全部やろうとしたら…コードが複雑になりすぎてしまいます。

悪い例:責任が多すぎるサザエさんクラス

class Sazae:
    def __init__(self):
        self.name = "サザエ"
        self.budget = 50000
    
    def cook(self):
        # 料理をする
        pass
    
    def do_laundry(self):
        # 洗濯をする
        pass
    
    def clean_house(self):
        # 掃除をする
        pass
    
    def go_shopping(self):
        # 買い物をする
        pass
    
    def manage_budget(self):
        # 家計簿をつける
        pass
    
    def take_care_of_tarao(self):
        # タラちゃんの世話をする
        pass

このクラスは、料理方法を変更したいとき、家計簿のロジックを変更したいとき、掃除の方法を変更したいときなど、様々な理由で修正が必要になります。これは単一責任の原則に違反しています。

良い例:責任を分離

class Cook:
    """料理専門クラス"""
    def prepare_meal(self, ingredients):
        return f"{ingredients}で料理を作りました"

class BudgetManager:
    """家計管理専門クラス"""
    def __init__(self, budget):
        self.budget = budget
    
    def record_expense(self, amount, category):
        self.budget -= amount
        return f"{category}に{amount}円使いました。残高: {self.budget}円"

class Childcare:
    """子育て専門クラス"""
    def take_care(self, child_name):
        return f"{child_name}のお世話をしました"

それぞれのクラスが1つの明確な責任を持つことで、変更の影響範囲が限定され、理解しやすく保守しやすいコードになります。

ポイント

単一責任の原則を守ることで、コードの変更が必要になったとき、影響を受ける範囲を最小限に抑えられます。1つのクラスが複数の理由で変更される状況は避けるべきです。

O: 開放閉鎖の原則(Open/Closed Principle)

原則の内容

「ソフトウェアのエンティティ(クラス、モジュールなど)は、拡張に対して開いていて、修正に対して閉じているべき」という原則です。つまり、既存のコードを変更せずに、新しい機能を追加できるべきということです。

サザエさんで理解する

磯野家には、サザエさん、カツオ、ワカメ、タラちゃんなど様々な家族がいます。新しい家族が増えたとき、既存の家族クラスを修正せずに対応できるようにするのが理想的です。

悪い例:新しいキャラクターを追加するたびに修正が必要

class FamilyIntroduction:
    def introduce(self, character_type):
        if character_type == "sazae":
            return "私はサザエです。24歳の主婦です"
        elif character_type == "katsuo":
            return "僕はカツオ。小学5年生です"
        elif character_type == "wakame":
            return "私はワカメ。小学3年生です"
        # 新キャラが増えるたびにこのメソッドを修正する必要がある

この実装では、タマやイクラちゃんなど新しいキャラクターが登場するたびに、introduceメソッドを修正しなければなりません。

良い例:継承を使った拡張可能な設計

from abc import ABC, abstractmethod

class FamilyMember(ABC):
    """家族メンバーの基底クラス"""
    @abstractmethod
    def introduce(self):
        pass

class Sazae(FamilyMember):
    def introduce(self):
        return "私はサザエです。24歳の主婦です"

class Katsuo(FamilyMember):
    def introduce(self):
        return "僕はカツオ。小学5年生です"

class Wakame(FamilyMember):
    def introduce(self):
        return "私はワカメ。小学3年生です"

# 新しいキャラクターを追加する場合(既存コードは変更不要)
class Tama(FamilyMember):
    def introduce(self):
        return "ニャー(タマです)"

この設計なら、タマやイクラちゃんを追加する際も、既存のクラスを修正する必要がありません。新しいクラスを追加するだけで対応できます。

ポイント

開放閉鎖の原則により、機能追加の際にバグを混入させるリスクが減ります。既存コードをいじらないため、すでに動いている部分に影響を与えません。継承やインターフェースを活用することで実現できます。

L: リスコフの置換原則(Liskov Substitution Principle)

原則の内容

「派生クラス(サブクラス)は、基底クラス(スーパークラス)と置き換え可能でなければならない」という原則です。親クラスを使っている箇所で、子クラスを代わりに使っても正しく動作すべきということです。

サザエさんで理解する

磯野家とフグ田家の「家族」という概念を考えてみましょう。どちらも家族なので、「家族として振る舞う」という共通の特性を持っているはずです。

悪い例:置き換えできない継承

class IsFamily:
    def live_together(self):
        return "一緒に暮らしています"
    
    def have_dinner_together(self):
        return "みんなで夕食を食べます"

class IsonoFamily(IsFamily):
    def live_together(self):
        return "磯野家で一緒に暮らしています"

class Tama(IsFamily):
    def have_dinner_together(self):
        # タマは猫なので、人間と同じように食卓を囲まない
        raise Exception("タマは猫用のご飯を別で食べます")

この例では、TamaクラスがIsFamilyの振る舞いを期待どおりに実現していません。have_dinner_together()メソッドで例外を投げているため、基底クラスと置き換えて使えません。

良い例:適切な継承関係

class LivingBeing(ABC):
    """生き物の基底クラス"""
    @abstractmethod
    def eat(self):
        pass

class Human(LivingBeing):
    def eat(self):
        return "食卓で食事をします"
    
    def communicate(self):
        return "言葉で話します"

class Pet(LivingBeing):
    def eat(self):
        return "ペット用のご飯を食べます"

class Sazae(Human):
    def eat(self):
        return "サザエさんが食卓で食事をします"

class Tama(Pet):
    def eat(self):
        return "タマが猫用のご飯を食べます"

この設計では、HumanPetを適切に分離し、それぞれがLivingBeingとして置き換え可能です。タマは人間ではなくペットとして正しく分類されています。

ポイント

リスコフの置換原則は、継承を使う際の指針です。「〜は〜の一種である(is-a関係)」が本当に成り立つかを慎重に考える必要があります。無理な継承は避け、適切な抽象化を行いましょう。

I: インターフェース分離の原則(Interface Segregation Principle)

原則の内容

「クライアントは、自分が使わないインターフェースへの依存を強制されるべきではない」という原則です。大きな万能インターフェースよりも、小さく特化したインターフェースを複数持つべきということです。

サザエさんで理解する

磯野家の人々には様々な役割があります。全員が全ての役割をこなせるわけではないのに、全員に全ての能力を要求するのは不適切です。

悪い例:巨大すぎるインターフェース

from abc import ABC, abstractmethod

class AllAbilities(ABC):
    """すべての能力を要求する巨大インターフェース"""
    @abstractmethod
    def work(self):
        pass
    
    @abstractmethod
    def do_housework(self):
        pass
    
    @abstractmethod
    def study(self):
        pass
    
    @abstractmethod
    def play(self):
        pass

class Namihei(AllAbilities):
    def work(self):
        return "会社で働きます"
    
    def do_housework(self):
        # 波平さんは普段家事をしないが、実装を強制される
        return "たまに手伝います"
    
    def study(self):
        # 波平さんは学生ではないが、実装を強制される
        return "新聞を読みます"
    
    def play(self):
        return "囲碁をします"

この設計では、波平さんが実際には行わないstudy()などのメソッドも実装を強制されてしまいます。

良い例:小さく特化したインターフェース

from abc import ABC, abstractmethod

class Worker(ABC):
    """働く能力のインターフェース"""
    @abstractmethod
    def work(self):
        pass

class Student(ABC):
    """学ぶ能力のインターフェース"""
    @abstractmethod
    def study(self):
        pass

class Homemaker(ABC):
    """家事をする能力のインターフェース"""
    @abstractmethod
    def do_housework(self):
        pass

# 波平さんは働く人として実装
class Namihei(Worker):
    def work(self):
        return "会社で働きます"

# カツオは学生として実装
class Katsuo(Student):
    def study(self):
        return "宿題をします(しぶしぶ)"

# サザエさんは主婦として実装
class SazaeHomemaker(Homemaker):
    def do_housework(self):
        return "家事をこなします"

このように、必要な能力だけを持つ小さなインターフェースに分離することで、各クラスは自分に必要なインターフェースだけを実装すればよくなります。

ポイント

インターフェース分離の原則により、クラスは本当に必要な機能だけに依存できます。使わない機能への依存を避けることで、変更の影響を受けにくくなり、コードの理解も容易になります。

D: 依存性逆転の原則(Dependency Inversion Principle)

原則の内容

「上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象に依存すべきである」という原則です。具体的な実装ではなく、抽象(インターフェースや抽象クラス)に依存することで、柔軟性の高い設計を実現します。

サザエさんで理解する

サザエさんが買い物に行く場面を考えてみましょう。サザエさんは特定のお店(三河屋)だけでなく、様々なお店で買い物できるはずです。

悪い例:具体的な実装に依存

class Mikawaya:
    """三河屋(具体的な店)"""
    def sell(self, item):
        return f"三河屋で{item}を購入しました"

class SazaeShopper:
    def __init__(self):
        # 三河屋に直接依存している
        self.shop = Mikawaya()
    
    def buy_groceries(self, item):
        return self.shop.sell(item)

この実装では、サザエさんは三河屋にしか買い物に行けません。別のお店で買い物したい場合、SazaeShopperクラス自体を修正する必要があります。

良い例:抽象に依存

from abc import ABC, abstractmethod

class Shop(ABC):
    """お店の抽象インターフェース"""
    @abstractmethod
    def sell(self, item):
        pass

class Mikawaya(Shop):
    def sell(self, item):
        return f"三河屋で{item}を購入しました"

class Supermarket(Shop):
    def sell(self, item):
        return f"スーパーで{item}を購入しました"

class ConvenienceStore(Shop):
    def sell(self, item):
        return f"コンビニで{item}を購入しました"

class SazaeShopper:
    def __init__(self, shop: Shop):
        # 抽象(Shop)に依存している
        self.shop = shop
    
    def buy_groceries(self, item):
        return self.shop.sell(item)

# 使用例
# 三河屋で買い物
sazae_at_mikawaya = SazaeShopper(Mikawaya())
print(sazae_at_mikawaya.buy_groceries("醤油"))

# スーパーで買い物(SazaeShopperクラスは変更不要)
sazae_at_supermarket = SazaeShopper(Supermarket())
print(sazae_at_supermarket.buy_groceries("野菜"))

この設計では、SazaeShopperShopという抽象に依存しています。新しいお店が増えても、SazaeShopperクラスを変更する必要はありません。

ポイント

依存性逆転の原則により、コードの柔軟性と再利用性が向上します。具体的な実装を簡単に差し替えられるため、テストも容易になります。依存性注入(Dependency Injection)のパターンと組み合わせることで、より保守性の高いコードが実現できます。

まとめ:SOLID原則がもたらすメリット

サザエさんの世界を通じて、SOLID原則の5つの考え方を見てきました。これらの原則は以下のようなメリットをもたらします。

保守性の向上

各クラスの責任が明確になり、コードの変更が必要な場所を素早く特定できます。磯野家の家事を分担するように、コードの責任も適切に分担しましょう。

拡張性の確保

新機能を追加する際に、既存のコードを変更せずに済みます。新しい家族が増えても、既存の家族の生活が変わらないように、コードも安定性を保てます。

テストの容易さ

小さく分離されたコンポーネントは、個別にテストしやすくなります。依存性が抽象化されているため、モックやスタブを使った単体テストも簡単です。

チーム開発での効率化

明確な設計原則により、チームメンバー間でコードの品質基準を共有できます。レビューもしやすくなり、技術的負債の蓄積を防げます。

おわりに

SOLID原則は、一見すると複雑に感じるかもしれませんが、サザエさんの日常のような身近な例で考えると理解しやすくなります。

最初からすべての原則を完璧に適用する必要はありません。まずは単一責任の原則から意識して、徐々に他の原則も取り入れていきましょう。磯野家の人々が自然に役割分担しているように、コードも自然な形で責任を分担できるようになれば、それが良い設計の第一歩です。

良いコード設計を身につけて、保守性の高い、長く愛されるソフトウェアを作っていきましょう。サザエさんが50年以上も愛され続けているように、あなたのコードも長く使われる資産となるはずです。

フリーランスボード

20万件以上の案件から、副業に最適なリモート・週3〜の案件を一括検索できるプラットフォーム。プロフィール登録でAIスカウトが自動的にマッチング案件を提案。市場統計や単価相場、エージェントの口コミも無料で閲覧可能なため、本業を続けながら効率的に高単価の副業案件を探せます。フリーランスボード

ITプロパートナーズ

週2〜3日から働ける柔軟な案件が業界トップクラスの豊富さを誇るフリーランスエージェント。エンド直契約のため高単価で、週3日稼働でも十分な報酬を得られます。リモートや時間フレキシブルな案件も多数。スタートアップ・ベンチャー中心で、トレンド技術を使った魅力的な案件が揃っています。専属エージェントが案件紹介から契約交渉までサポート。利用企業2,000社以上の実績。ITプロパートナーズ

Midworks 10,000件以上の案件を保有し、週3日〜・フルリモートなど柔軟な働き方に対応。高単価案件が豊富で、報酬保障制度(60%)や保険料負担(50%)など正社員並みの手厚い福利厚生が特徴。通勤交通費(月3万円)、スキルアップ費用(月1万円)の支給に加え、リロクラブ・freeeが無料利用可能。非公開案件80%以上、支払いサイト20日で安心して稼働できます。Midworks

らくらくPython塾 – 読むだけでマスター