Menu
Coddy logo textTech

継承よりコンポジション

CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 41/70。

継承はクラス間に密な結合を生み出します。CarVehicle を継承している場合、Vehicle に対するいかなる変更も Car を壊してしまう可能性があります。コンポジションは、より柔軟な代替案を提供します。振る舞いを継承する代わりに、クラスはその振る舞いを提供するオブジェクトを包含します。

移動して攻撃できるゲームのキャラクターを考えてみましょう。継承を使用すると、複雑な階層を作成することになるかもしれません。コンポジションを使用すると、キャラクターに必要なコンポーネントを与えるだけで済みます:

public class MovementBehavior
{
    public void Move() => Console.WriteLine("Moving...");
}

public class AttackBehavior
{
    public void Attack() => Console.WriteLine("Attacking!");
}

public class Character
{
    private MovementBehavior movement = new MovementBehavior();
    private AttackBehavior attack = new AttackBehavior();
    
    public void Move() => movement.Move();
    public void Attack() => attack.Attack();
}

主な違いはその関係性にあります。継承は「is-a」関係(Dog Animal である)を表し、一方、コンポジションは「has-a」関係(Character 移動の振る舞いを持っている)を表します。コンポジションを使用すると、実行時に振る舞いを入れ替えたり、機能を自由に組み合わせたり、壊れやすい基底クラス(fragile base class)問題を回避したりすることができます。

真正な階層関係がある場合には、継承を使用してください。柔軟性が必要な場合、無関係なクラス間で振る舞いが共有される可能性がある場合、または振る舞いを動的に変更したい場合には、コンポジションを選択してください。

challenge icon

チャレンジ

簡単

継承よりもコンポジション(合成)の力を示す通知システムを構築しましょう。通知クラスの複雑な階層を作成する代わりに、交換可能な振る舞いコンポーネントを NotificationSender に持たせることで構成します。

コードは3つのファイルに分けて整理します:

  • Behaviors.cs: Notifications 名前空間内に、通知送信の異なる側面を処理する2つの振る舞いクラスを作成します:
    • FormattingBehavior - メッセージの先頭に [ALERT] を、末尾に [END] を付けて返す Format(string message) メソッドを持ちます。
    • DeliveryBehavior - Sending via {channel}: {message} という形式の文字列を返す Deliver(string channel, string message) メソッドを持ちます。
  • NotificationSender.cs: コンポジションを使用して振る舞いを組み合わせる NotificationSender クラスを Notifications 名前空間に作成します。この送信者は以下の機能を持つ必要があります:
    • FormattingBehaviorDeliveryBehavior の両方のインスタンスをプライベートフィールドとして保持する
    • 配信チャネル名を格納する Channel プロパティ(string)を持つ
    • チャネル名を受け取るコンストラクタを持つ
    • Send(string message) メソッドを持ち、まずフォーマットの振る舞いを使用してメッセージを整形し、次に配信の振る舞いを使用してチャネル経由で配信し、最終的な結果を返す
  • Program.cs: メインファイルで、入力されたチャネルを使用して NotificationSender を作成し、入力されたメッセージを送信します。構成された振る舞いがどのように連携するかを示すために、結果をプリントしてください。

2つの入力を受け取ります:

  • 配信チャネル名(例:EmailSMS
  • 送信するメッセージ

通知送信者の Send() を呼び出した結果を出力してください。

例えば、入力が EmailServer is down の場合、出力は以下のようになります:

Sending via Email: [ALERT] Server is down [END]

NotificationSender が何からも継承していないことに注目してください。これは単にフォーマットと配信の振る舞いを「持っている」だけです。このコンポジションのアプローチにより、送信者クラスを変更することなく、どちらの振る舞いも簡単に交換したり、これらの振る舞いを全く別のクラスで再利用したりすることができます。

チートシート

コンポジションは継承に代わる手法であり、親クラスから振る舞いを継承するのではなく、振る舞いを提供するオブジェクトをクラスに含めることで、より柔軟なコードを作成します。

継承 vs コンポジション:

  • 継承は「is-a」関係を表します(犬は動物である
  • コンポジションは「has-a」関係を表します(キャラクターは移動の振る舞いを持っている

コンポジションの利点:

  • 実行時に振る舞いを入れ替えることができる
  • 機能を自由に組み合わせることができる
  • 密結合や壊れやすい基底クラスの問題を回避できる
  • 関連のないクラス間で振る舞いを共有できる

振る舞いコンポーネントを使用した例:

public class MovementBehavior
{
    public void Move() => Console.WriteLine("Moving...");
}

public class AttackBehavior
{
    public void Attack() => Console.WriteLine("Attacking!");
}

public class Character
{
    private MovementBehavior movement = new MovementBehavior();
    private AttackBehavior attack = new AttackBehavior();
    
    public void Move() => movement.Move();
    public void Attack() => attack.Attack();
}

各アプローチをいつ使用するか:

  • 真の階層関係には継承を使用する
  • 柔軟性、共有される振る舞い、または動的な振る舞いの変更にはコンポジションを選択する

自分で試してみよう

using System;
using Notifications;

class Program
{
    public static void Main(string[] args)
    {
        // 入力を読み込む
        string channel = Console.ReadLine();
        string message = Console.ReadLine();

        // TODO: `channel` を使用して `NotificationSender` を作成する
        
        // TODO: メッセージを送信し、結果を表示する
        
    }
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン