C#オブジェクト指向チュートリアル――「仕事を頼む」からクラス設計を学ぶ

広告

C#のクラスやメソッドの文法は覚えたものの、「どのクラスに、どの処理を書けばよいのか分からない」と感じることはないでしょうか。

このチュートリアルでは、オブジェクト指向を「継承を使うこと」ではなく、役割を持ったオブジェクトに仕事を頼むこととして学びます。小さな車のプログラムを段階的に作り、メッセージ、責務、委譲、インターフェース、コンポジションまでをつなげます。

このチュートリアルのゴール

最終的に、次のような関係をC#で表せるようになることが目標です。

  • 利用者はCarへ「走り始めて」と頼む
  • Carはエンジン始動をIEngineへ任せる
  • ガソリンエンジンと電気モーターを差し替えられる
  • 各クラスが自分の責務だけを担当する

完成形を先に考えるのではなく、「誰に、どんな仕事を頼むか」を一段ずつコードにしていきます。

準備

Visual StudioまたはVisual Studio Codeで、C#のコンソールアプリを一つ作成してください。このチュートリアルのコードは、すべてProgram.csへ書いて試せます。

最初は「クラスをきれいに分ける」ことを急がず、動作を確認しながら役割を見つけていきます。

ステップ1:オブジェクトへ仕事を頼む

まず、エンジンを表すEngineクラスを作ります。

Engine engine = new Engine();
engine.Start();

class Engine
{
    public void Start()
    {
        Console.WriteLine("エンジンが始動しました");
    }
}

engine.Start()は、単なるメソッド呼び出しとして見ることもできます。オブジェクト指向では、engineオブジェクトへ「始動して」とメッセージを送っていると考えます。

ここで確認すること

  • メッセージを受け取る相手はengine
  • お願いしている仕事はStart
  • 始動方法を知っているのはEngine

呼び出す側は、点火などの具体的な手順を知りません。相手に希望を伝え、実際の処理は相手へ任せています。

ステップ2:Carの責務を考える

次に、車を表すCarクラスを追加します。利用者が直接エンジンを操作するのではなく、車へ「走り始めて」と頼める形にします。

Engine engine = new Engine();
Car car = new Car(engine);

car.Start();

class Car
{
    private readonly Engine _engine;

    public Car(Engine engine)
    {
        _engine = engine;
    }

    public void Start()
    {
        Console.WriteLine("車が走り始めます");
        _engine.Start();
    }
}

class Engine
{
    public void Start()
    {
        Console.WriteLine("エンジンが始動しました");
    }
}

実行結果は次のとおりです。

車が走り始めます
エンジンが始動しました

Carは、車として利用者からの依頼を受け付けます。ただし、エンジンを始動する詳しい処理まで自分で抱えず、Engineへ任せます。このように、受け取った仕事の一部を別のオブジェクトへ頼むことを委譲と呼びます。

利用者がCarへ走り始めるよう依頼し、CarがEngineへ始動を委譲する流れ
利用者からCar、CarからEngineへメッセージを送り、仕事を委譲する

責務を一言で表す

クラス責務
Car車として「走り始めて」という依頼を受け付ける
Engineエンジンを始動する

責務を短い言葉で説明できると、処理をどこに置くべきか判断しやすくなります。

ステップ3:具体的なクラスへの依存を見直す

現在のCarは、具体的なEngineクラスを知っています。ここで「電気自動車にも同じCarを使いたい」という要求が追加されたとします。

ガソリンエンジンと電気モーターでは、始動の仕組みが異なります。しかし、Carが相手へ頼みたいことは、どちらも「始動して」です。

相手が何者かではなく、どんなメッセージに応答できるかに注目します。

ステップ4:インターフェースで役割を表す

「始動できる」という役割を、IEngineインターフェースとして表します。

interface IEngine
{
    void Start();
}

class GasolineEngine : IEngine
{
    public void Start()
    {
        Console.WriteLine("ガソリンエンジンが始動しました");
    }
}

class ElectricMotor : IEngine
{
    public void Start()
    {
        Console.WriteLine("電気モーターが静かに起動しました");
    }
}

IEngineは「どのように始動するか」を決めません。「Startというメッセージに応答できる」という約束だけを表します。

これがインターフェースの大切な役割です。共通データをまとめるためではなく、相手に期待する能力を表すために使えます。

ステップ5:コンポジションで部品を差し替える

Carが受け取る型をIEngineへ変更します。

class Car
{
    private readonly IEngine _engine;

    public Car(IEngine engine)
    {
        _engine = engine;
    }

    public void Start()
    {
        Console.WriteLine("車が走り始めます");
        _engine.Start();
    }
}

Carは、内部で使う部品を継承によって固定するのではなく、外から受け取っています。オブジェクトを組み合わせて機能を作るこの考え方がコンポジションです。

Carは、渡された相手がIEngineの約束を守っていることだけを知っています。ガソリンなのか電気なのかを判断するif文も必要ありません。

CarがIEngineを介してGasolineEngineとElectricMotorを差し替える構成図
IEngineという共通の役割を介して、エンジンの実装を差し替える

完成コード

ここまでの内容を、一つのプログラムにまとめます。

Car gasolineCar = new Car(new GasolineEngine());
Car electricCar = new Car(new ElectricMotor());

gasolineCar.Start();
Console.WriteLine();

electricCar.Start();

interface IEngine
{
    void Start();
}

class GasolineEngine : IEngine
{
    public void Start()
    {
        Console.WriteLine("ガソリンエンジンが始動しました");
    }
}

class ElectricMotor : IEngine
{
    public void Start()
    {
        Console.WriteLine("電気モーターが静かに起動しました");
    }
}

class Car
{
    private readonly IEngine _engine;

    public Car(IEngine engine)
    {
        _engine = engine;
    }

    public void Start()
    {
        Console.WriteLine("車が走り始めます");
        _engine.Start();
    }
}

実行すると、次のように表示されます。

車が走り始めます
ガソリンエンジンが始動しました

車が走り始めます
電気モーターが静かに起動しました

Carのコードを変更しなくても、組み合わせる部品を変えるだけで振る舞いを切り替えられました。これがポリモーフィズムです。

メッセージの流れを言葉で確認する

  1. 利用者がcar.Start()Carへ仕事を頼む
  2. Carが依頼を受け取り、車としての処理を行う
  3. Car_engine.Start()でエンジンへ仕事を任せる
  4. GasolineEngineまたはElectricMotorが自分の方法で応答する

クラス図や専門用語を先に覚えなくても、この流れを説明できれば設計の中心は理解できています。

よくある書き方との違い

Carがすべての処理を持つ

Carの中へガソリンと電気の始動処理をすべて書くと、車種が増えるたびにCarを変更することになります。責務も増え、クラスが大きくなります。

型をif文で判定する

エンジンの種類を文字列や列挙型で判定すると、新しい種類を追加するたびに分岐を修正します。今回は、同じメッセージにそれぞれのオブジェクトが応答する形にしました。

最初から継承ツリーを作る

「車だから何を継承するか」から始めると、現実世界の分類をコードへ写すことが目的になりがちです。まず必要なメッセージと責務を見つけ、必要になった仕組みだけを選ぶ方が設計しやすくなります。

練習問題

問題1:停止する処理を追加する

IEngineStopメソッドを追加し、Car.Stop()から仕事を委譲してください。ガソリンと電気で異なるメッセージを表示してみましょう。

問題2:新しいエンジンを追加する

HybridEngineを追加してください。既存のCarを変更せずに使えれば成功です。

問題3:責務を説明する

CarIEngine、各エンジンクラスの責務を、それぞれ一文で書いてください。説明が長くなるクラスは、複数の責務を抱えていないか見直します。

最後に整理

用語このチュートリアルでの意味
オブジェクト仕事をお願いできる相手
メッセージStartなど、相手への依頼
責務そのオブジェクトが応答する仕事
委譲受け取った仕事の一部を別の相手へ頼むこと
インターフェース相手に期待する能力の約束
コンポジションオブジェクトを組み合わせて機能を作ること
ポリモーフィズム同じメッセージに異なる方法で応答できること

まとめ

オブジェクト指向の設計では、最初から継承やデザインパターンを使う必要はありません。まず「誰に、どんな仕事を頼みたいか」を考えます。

オブジェクト指向は、役割を持ったオブジェクト同士の会話として捉えると理解しやすくなります。

メッセージと責務を起点にすると、インターフェースやコンポジション、ポリモーフィズムも、文法上の機能ではなく「変更しやすい協力関係を作るための道具」としてつながります。

訪問数 4 回, 今日の訪問数 4回

広告