Pythonでクラスを書き始めると、最初に出会う大きな壁が「クラス同士の関係をどう設計するか」という問題です。共通部分があるからとりあえず継承でつないでみたら、後から仕様変更が入った瞬間にコード全体が崩壊した……そんな苦い経験、ありませんか?😱
そんな悩みを解決するキーワードが「is-a(イズア)の関係」と「has-a(ハズア)の関係」。この2つの違いを理解すれば、明日からのクラス設計に確かな羅針盤が手に入り、半年後・1年後の自分が困らないコードを書けるようになります✨
プログラミングの分野で使われる「クラス同士の関係」とは、複数のクラスがどう結びついているかを表す考え方のことです。代表的なものに「is-a」と「has-a」の2種類があり、どちらを使うかでコードの構造も保守性も大きく変わります。
言葉の説明だけだと分かりにくいですよね。聞き慣れないと難しそうに感じるかもしれませんが、考え方はとってもシンプルです。
is-a(A is a B)は、「AはBです」「AはBの一種だ」という関係を表します。日常の例で考えるとわかりやすいですよ。
「アルコール飲料」という大きなカテゴリの中の一つが「ワイン」、というイメージですね。これをPythonで実装するときには、クラスの継承を使います。基底クラスがB、派生クラスがAという関係です。
たとえば、ユーザークラスと管理者ユーザークラスがあるとします。
class User:
def __init__(self, user_id):
self.user_id = user_id
def login(self): ...
def logout(self): ...
def create_blog(self): ...
class AdminUser(User):
def remove_blog(self): ...
def remove_user(self): ... 「管理者ユーザーはユーザーの一種」なので、AdminUser は User を継承して、追加の権限(ブログ削除・ユーザー削除)を持たせる――きれいに整理されたis-aの関係ですね👌
has-a(A has a B)は、「AはBを持っています」という関係を表します。これも例で見るとイメージしやすいです。
これをPythonで実装するときには、クラスのインスタンス変数として別のクラスのオブジェクトを保持する方法を使います。これは「コンポジション」とも呼ばれる設計手法です。
ショッピングカートと商品の関係で考えてみましょう。
class Item:
def __init__(self, item_id, item_name, item_price):
self.item_id = item_id
self.item_name = item_name
self.item_price = item_price
class ShoppingCart:
def __init__(self):
self.item_list = []
def add_item(self, item: Item):
self.item_list.append(item)
def remove_item(self, item: Item):
self.item_list.remove(item) ショッピングカートは商品を「持っている」――まさにhas-aの関係。型ヒント(item: Item)を付けておくと、引数に何を渡すべきかが一目で分かって読みやすさもアップします📝
ここからが本記事の核心です。is-aは直感的で美しく見えますが、実は「変更に弱い」という大きな欠点を抱えています。
先ほどの User → AdminUser の設計に、新しく「スタッフユーザー」を追加したくなったとします。スタッフユーザーは、ログイン・ログアウトはできるけれどブログ作成はできず、閲覧だけができるとします。
「スタッフユーザーはユーザー」だから、素直に User を継承したくなります。でも継承した瞬間、create_blog() メソッドまで引き継いでしまい、「ブログを作れないはずのスタッフが作れてしまう」というバグが生まれます😱
これを直そうと思うと、Userクラスからcreate_blog()を削除し、新たに「一般ユーザークラス」を作ってcreate_blog()を移し、既存コード内のUserオブジェクトをすべて一般ユーザークラスに書き換え……という大規模リファクタリングが必要になります。
小さなクラスならまだしも、大規模なアプリケーションでこれをやるのは想像するだけで気が遠くなりますよね……。
一方、has-aの関係はクラス同士のつながりが「ゆるい」ので、片方の変更がもう片方に波及しにくいという大きなメリットがあります。「便利さ」という点ではis-aに比べて圧倒的に優位なんです🎉
とはいえ、is-aを使ってはいけないわけではありません。コード量を減らせる、ポリモーフィズム(オブジェクト指向の重要概念)が使える、といった大きなメリットもあります。
is-aは意識的に注意しながら使う必要があり、欠点はあるものの、コード量が少なくなったりポリモーフィズムが使えるなどの大きなメリットがあります。
使い分けの目安としては、次のように考えるとスッキリします。
近年のソフトウェア設計界隈では「継承よりコンポジションを優先せよ(Favor composition over inheritance)」という格言が広く知られていますが、まさにこの考え方の根っこにあるのがis-a/has-aの関係性なんです🌟
is-aとhas-aの感覚をつかんだら、次は体系的にクラス設計とオブジェクト指向を学びたいところ。書籍で骨太な知識を入れることで、現場で「この設計でいいのかな?」と迷ったときの判断軸が確実に身につきます📚
Pythonの文法からクラス・継承・ポリモーフィズムまでを段階的に学べる定番入門書。「クラスってそもそも何?」という段階の人でも、安心して土台を固められます🌱
SOLID原則、デザインパターン、リファクタリングなど、is-a/has-aの設計判断を支える理論的バックボーンを総合的に学べる一冊。「なぜ継承よりコンポジションが推奨されるのか」が腹落ちします🧠
クラス・プロトコル・データクラス・抽象基底クラスといった現代Pythonのクラス設計に必要な要素を、深く・実践的に解説した名著。中級から上級への橋渡しに最適な一冊です🚀
型ヒント・抽象クラス・プロトコル・テストなど、保守性の高いPythonコードを書くための実践知が凝縮された一冊。「変更に弱いis-a」を防ぐ具体的なテクニックも学べます🛡️
コンポジション・ストラテジー・ファクトリーなど、has-aの関係を巧みに活かした設計パターンが体系的にまとまった一冊。「is-aで詰んだとき、こう書き換えればいいのか!」という発見の連続です💡
基本的にはその方針で大きく外しません。「明らかに同じ種類だ」と確信できる場合だけis-aを使い、それ以外はhas-aで設計しておくと、後から仕様変更が入っても柔軟に対応しやすくなります。
Pythonは多重継承をサポートしていますが、複雑さが指数関数的に増す上に、メソッド解決順序(MRO)でハマりやすくなります。多重継承よりも、Mix-inやプロトコル、コンポジションを組み合わせた方が、結果的にシンプルで保守しやすい設計になります。
「明確に共通のインターフェースを強制したい」場面ではABCが有効です。abc.ABC を継承し @abstractmethod を付けると、サブクラスに特定のメソッド実装を強制できます。is-aとhas-aの中間的な「契約による設計」を実現したいときに役立ちます。
必須ではありません。Pythonはダックタイピングの言語なので、同じメソッド名さえ持っていれば継承関係になくてもポリモーフィックに扱えます。typing.Protocol を使えば、より型安全にダックタイピングを実現できます🦆
継承で書かれているコードをコンポジションに書き換える場合、テストを充実させてから少しずつ進めるのが鉄則です。一気に書き換えようとせず、1クラスずつ・1メソッドずつ移行することで、リスクを最小化できます🛠️
is-aとhas-a――この2つの関係性を理解しているかどうかで、あなたのクラス設計は劇的に変わります。「とりあえず継承」から卒業し、「この関係はis-aで本当に成り立つか?has-aで書いた方が柔軟じゃないか?」と一歩立ち止まって考えられるようになれば、あなたはもう設計者として一段上のステージにいます🌟
動画や記事で得たエッセンスを、書籍で体系的に深めれば、あなたのオブジェクト指向設計力は一生モノの財産になります。今日紹介した5冊から気になる一冊を手に取って、「変更に強いコードを書ける開発者」への第一歩を踏み出してみてください📘🚀