3
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

 オブジェクト指向をより実践的に理解するため、ある課題を題材にして、Pythonのコードを使って「継承」と「コンポジション」について解説します。
 個人開発時、動けばいいとコードを書いていましたが、保守しやすいコードが必要であり、基礎を学ぶことの重要性を感じたため今回の記事を書くことにしました。
 今回の記事の出発点でもあるオブジェクト指向に関してはQiitaにて投稿していますので、この記事での説明は割愛します。

駆け出しエンジニアであるため間違いや改善点等あれば、ぜひお願いします!

対象読者

  • プログラミングを学び始めた方
  • Pythonの知識を深めたい方
  • オブジェクト指向ってなんだ?と迷子な方
  • AIに頼りすぎて、自分のコードがわからなくなってしまった方(←私ww)
    ※ 細かいPythonの書き方については解説しません

継承とコンポジションとは

継承
ある親クラスの機能を子クラスが引き継ぐ仕組み(クラス同士がis aの関係)。これを使用すると共通機能を何度も書く必要がなくなり、コードの再利用性や拡張性が高まる

コンポジション(構成)
オブジェクトが別のオブジェクトを部品として持つ仕組み(has aの関係)

といっても、よくわからないのでコードを見つつ解説していきます

課題

下記の課題を題材に継承とコンポジションを使ったコードを書いてみます!

要約: 複数クラスに所属する生徒の成績データ(生徒名と点数)を受け取り、クラスごとと学校全体の平均点を計算して表示するプログラムを作成する。

詳細:各クラスには複数の生徒が在籍しており、生徒は「名前」と「点数(数値)」を持つ。
各クラスごとに生徒の得点を表示し、クラス平均を計算して表示する。
全クラスの処理が終わったら、学校全体の平均点を計算・表示する。

入力例
classes = {
    "1年A組": {"佐藤": 85, "鈴木": 92, "高橋": 78},
    "1年B組": {"田中": 88, "渡辺": 76, "山本": 90, "中村": 85},
    "1年C組": {"伊藤": 80, "山田": 83}
}
出力例
1年A組の成績処理を開始します
佐藤: 85
鈴木: 92
高橋: 78
1年A組の平均点: 85.00点
(以下略)
学校全体の平均点: 83.75点

実際のコード

継承
この例では「クラス」も「学校」も「成績を集計できる存在」という共通の性質を持っているため、そのふるまいを継承で表現しています

class GradeAggregate:
    """
    成績を集計する(抽象的な概念)
    ・点数を追加できる
    ・平均点を計算できる
    """
    def __init__(self):
        self._total = 0
        self._count = 0
        
    @property
    def total(self):
        return self._total

    @property
    def count(self):
        return self._count

    def add_score(self, score: int):
        self._total += score
        self._count += 1

    @property
    def average(self):
        return self._total / self._count if self._count else 0


class SchoolClass(GradeAggregate):  # ←class クラス名(親クラス名)と書くことで親(GradeAggregate)が持っている属性と機能を引き継ぐ
    """
    クラス単位の成績処理
    SchoolClass is a GradeAggregate
    """
    def __init__(self, name: str):
        super().__init__()
        self.name = name

    def process_students(self, students: dict[str, int]):
        print(f"{self.name}の成績処理を開始します")

        for student_name, score in students.items():
            print(f"{student_name}: {score}")
            self.add_score(score)

        print(f"{self.name}の平均点: {self.average:.2f}\n")


class School(GradeAggregate):
    """
    学校全体の成績処理
    School is a GradeAggregate
    """
    def process_classes(self, classes: dict[str, dict[str, int]]):
        for class_name, students in classes.items():
            classroom = SchoolClass(class_name)
            classroom.process_students(students)

            # クラスの集計結果を学校に加算
            self._total += classroom._total
            self._count += classroom._count

        print(f"学校全体の平均点: {self.average:.2f}")


# ===== 実行例 =====

classes = {
    "1年A組": {"佐藤": 85, "鈴木": 92, "高橋": 78},
    "1年B組": {"田中": 88, "渡辺": 76, "山本": 90, "中村": 85},
    "1年C組": {"伊藤": 80, "山田": 83}
}

school = School()
school.process_classes(classes)

コンポジション
オブジェクトが他のオブジェクトをインスタンス変数として保持する仕組み
→クラスや学校は複数の生徒(オブジェクト)を持つ

class Student:
    """生徒を定義"""
    def __init__(self, name: str, score: int):
        self.name = name
        self.score = score

class SchoolClass:
    """クラスは複数の生徒を持つ(コンポジション)"""
    def __init__(self, name: str, students_data: dict):
        self.name = name
        self.students = [Student(n, s) for n, s in students_data.items()]

    def calculate_average(self): 
        if not self.students: return 0
        total = sum(s.score for s in self.students)
        return total / len(self.students)

    def display_report(self):
        print(f"{self.name}の成績処理を開始します")
        for s in self.students:
            print(f"{s.name}: {s.score}")
        print(f"{self.name}の平均点: {self.calculate_average():.2f}\n")

class School:
    """学校は複数のクラスを持つ(コンポジション)"""
    def __init__(self, all_data: dict):
        self.classes = [SchoolClass(name, data) for name, data in all_data.items()]

    def display_total_report(self):
        all_students = []
        for c in self.classes:
            c.display_report()
            all_students.extend(c.students)
        
        school_average = sum(s.score for s in all_students) / len(all_students)
        print(f"学校全体の平均点: {school_average:.2f}")

# ===== 実行 =====
classes = {
    "1年A組": {"佐藤": 85, "鈴木": 92, "高橋": 78},
    "1年B組": {"田中": 88, "渡辺": 76, "山本": 90, "中村": 85},
    "1年C組": {"伊藤": 80, "山田": 83}
}

school = School(classes)
school.display_total_report()

両者の比較

項目 継承 (Inheritance) コンポジション (Composition)
関係性 is-a (~は~の一種) has-a (~は~を持っている)
メリット 共通機能を一箇所に定義でき、親クラスの振る舞いをそのまま利用できる 部品ごとに独立しており、組み替えや再利用が容易
デメリット 親の変更が全ての子に影響する(密結合) 処理を仲介するコード(委譲)を書く必要がある
テスト 親と子が切り離せず、単体テストが複雑化しやすい 単一責務のクラスなため個別にテストができる

有名な原則に「Prefer composition over inheritance」というものがあるそうです

コンポジションの方が好まれるということのようですが、
これは上記の比較からもわかるように、親クラスの影響力が大きいことためです
またオブジェクト指向の考え方として、「もの」(データとそのふるまい)を実体化すべきという考え方によるもののようです

今回の例では生徒、クラス、学校をそれぞれのオブジェクトとして捉えることにより保守のしやすいコードになると考えました!

まとめ

その機能同士は、has-a (~は~を持っている)関係なのか、is-a (~は~の一種)の関係なのかを見極めて継承かコンポジションを選択していく必要があります(もちろんクラスを使わないという選択肢も)
今回はコンポジションが最適であると判断しましたが、内容によってその基準を決めていく基礎が必要だと改めて感じました。

本記事を通して、「なぜその設計を選ぶのか」を説明できることがオブジェクト指向を理解する第一歩だと実感しました!

参考記事

3
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?