基礎から学ぶC#入門 90日コース | 継承・抽象化・インターフェース - Day 46:依存関係を考える

C# 90日で身につけるC#
スポンサーリンク
スポンサーリンク

Day 46 のゴールと全体像

Day 46 のテーマは 「依存関係を考える」です。

キーワードはこの3つです。

  • クラス同士の依存
  • 疎結合
  • 依存を減らす考え方

ここまでで、クラスを作り、継承し、インターフェースで役割を分けるところまで来ました。 Day 46 では、もう一歩設計寄りに踏み込んで、

「クラス同士がベタベタにくっつきすぎないようにする」

という、とても大事な視点を身につけていきます。

クラス同士の「依存」とは何か

まずはざっくりイメージから

「依存」という言葉は少し堅いですが、 プログラムの世界では、

「あるクラスが、別のクラスに強く頼りすぎている状態」

だと思ってください。

たとえば、

  • OrderServiceSqlOrderRepository にベタッとくっついていて、 他のデータ保存方法に変えられない
  • UserServiceConsoleLogger を直接 new していて、 ログの出し方を差し替えにくい

といった状態は、 「依存が強い(密結合)」と言えます。

これが進むと、

  • ちょっと仕様を変えたいだけなのに、あちこちのクラスを直さないといけない
  • テストがしづらい
  • 拡張がしづらい

という「重たいコード」になっていきます。

密結合の例を見てみる

ログ出力を直接クラスに埋め込んでしまうパターン

まずは、あえて「よくない例」を見てみます。

using System;

// 密結合なサービスクラスの例
class UserService
{
    public void CreateUser(string name)
    {
        // ユーザー作成処理(ここでは簡略化)
        Console.WriteLine($"ユーザー「{name}」を作成しました。");

        // ログ出力を直接 Console.WriteLine で書いている
        Console.WriteLine($"[LOG] ユーザー作成: {name}");
    }
}
C#

このコードの問題点は、

  • ログの出し方が Console.WriteLine にべったり依存している
  • ログをファイルに出したくなったら、UserService の中身を直接書き換えないといけない
  • ログを無効化したいときも、やはり UserService を触る必要がある

というところです。

重要ポイント:

  • クラスの中で具体的な実装(Console.WriteLine など)を直接書いてしまうと、 そのクラスは「その実装」に強く依存することになります。

疎結合とは何か

「お互いを意識しすぎない関係」にする

疎結合(そけつごう)という言葉は、

「クラス同士が、できるだけゆるくつながっている状態」

を指します。

  • 具体的なクラス名にべったり依存しない
  • インターフェースや抽象クラスを介してやりとりする
  • 「何をしてほしいか」だけを意識し、「どうやるか」は相手に任せる

という状態を目指します。

これができると、

  • 実装を差し替えやすい
  • テストしやすい
  • 拡張しやすい

という「軽やかなコード」になっていきます。

依存を減らすための第一歩:インターフェースを挟む

ログ出力をインターフェースで抽象化する

Day 44 で登場した ILogger を使って、 依存を減らす考え方を実践してみます。

// ログ出力のインターフェース(契約)
interface ILogger
{
    void Log(string message);
}
C#

実装クラス:ConsoleLogger

using System;

// コンソールにログを出す実装
class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[Console] {message}");
    }
}
C#

疎結合な UserService

// 疎結合なサービスクラスの例
class UserService
{
    private readonly ILogger logger;

    // コンストラクタで ILogger を受け取る
    public UserService(ILogger logger)
    {
        this.logger = logger;
    }

    public void CreateUser(string name)
    {
        Console.WriteLine($"ユーザー「{name}」を作成しました。");

        // 具体的なログの出し方は知らない
        // 「ログを出してほしい」という依頼だけをする
        logger.Log($"ユーザー作成: {name}");
    }
}
C#

ここでの変化はこうです。

  • UserService は、もはや Console.WriteLine に依存していない
  • 依存しているのは「ILogger という契約」だけ
  • ログの出し方は、ILogger を実装したクラスに任せている

重要ポイント:

  • 具体クラスではなく、インターフェース(役割)に依存することで、 クラス同士の結びつきをゆるくできます。

実装の差し替えを試してみる

FileLogger を追加する

using System;
using System.IO;

// ファイルにログを出す実装
class FileLogger : ILogger
{
    private readonly string filePath;

    public FileLogger(string filePath)
    {
        this.filePath = filePath;
    }

    public void Log(string message)
    {
        var line = $"[{DateTime.Now}] {message}{Environment.NewLine}";
        File.AppendAllText(filePath, line);
    }
}
C#

呼び出し側で「どの実装を使うか」を選ぶ

class Program
{
    static void Main(string[] args)
    {
        // 1. コンソールにログを出す UserService
        ILogger consoleLogger = new ConsoleLogger();
        UserService consoleUserService = new UserService(consoleLogger);
        consoleUserService.CreateUser("山田");

        // 2. ファイルにログを出す UserService
        ILogger fileLogger = new FileLogger("log.txt");
        UserService fileUserService = new UserService(fileLogger);
        fileUserService.CreateUser("佐藤");

        Console.ReadLine();
    }
}
C#

ここで注目してほしいのは、

  • UserService のコードは一切変えていない
  • ログの出し方を変えたいときは、 「どの ILogger を渡すか」を変えるだけでよい

というところです。

これが、疎結合の力です。

依存を減らす考え方をステップで整理する

ステップ1:具体クラスへの直接依存を見つける

まずは、自分のコードの中で、

  • new ConsoleLogger()
  • new SqlOrderRepository()
  • new SomeConcreteClass()

のように、 「特定のクラス名にべったり依存している場所」を探します。

ステップ2:そのクラスが果たしている「役割」を言葉にする

次に、そのクラスが何をしているのかを、 一言で言い換えてみます。

  • ログを出している → ILogger
  • データを保存している → IRepository
  • メールを送っている → IMailer

など、 「役割の名前」を考えます。

ステップ3:役割をインターフェースとして切り出す

その役割を、インターフェースとして定義します。

interface IMailer
{
    void Send(string to, string subject, string body);
}
C#

ステップ4:クラスは「役割(インターフェース)」に依存するように書き換える

class NotificationService
{
    private readonly IMailer mailer;

    public NotificationService(IMailer mailer)
    {
        this.mailer = mailer;
    }

    public void Notify(string userEmail)
    {
        mailer.Send(userEmail, "お知らせ", "こんにちは!");
    }
}
C#

このように、

  • 具体クラスではなく、インターフェースに依存する
  • 実装の差し替えは「どのインスタンスを渡すか」で行う

という形にしていくと、 コード全体が疎結合になっていきます。

疎結合のメリットをもう少し深掘りする

1. テストしやすくなる

インターフェースに依存していると、 テスト用の「ダミー実装」を簡単に作れます。

// テスト用のダミーロガー
class DummyLogger : ILogger
{
    public void Log(string message)
    {
        // 何もしない、またはテスト用の記録だけする
    }
}
C#

これを UserService に渡せば、

  • ログ出力の副作用を気にせずテストできる
  • 「ログがちゃんと呼ばれているか」だけを確認することもできる

という状態になります。

2. 実装の変更に強くなる

  • ログをクラウドサービスに送るようにしたい
  • データ保存先をファイルからデータベースに変えたい

といったときも、

  • 新しい実装クラスを作る
  • インターフェースを実装する
  • 呼び出し側で渡すインスタンスを差し替える

だけで済みます。

3. セキュリティやパフォーマンスの改善もしやすい

セキュリティやパフォーマンスの観点から、

  • ログのフォーマットを変えたい
  • 保存方法を暗号化したい
  • 通信方法を変えたい

といったときも、 「実装クラスを差し替える」だけで対応できるようになります。

依存を減らすための簡易テンプレート

最後に、 「依存を減らす」ための基本テンプレートをまとめておきます。

役割のインターフェース

// 役割:通知を送る
interface INotifier
{
    void Notify(string message);
}
C#

実装A:コンソール通知

using System;

class ConsoleNotifier : INotifier
{
    public void Notify(string message)
    {
        Console.WriteLine($"[Console] {message}");
    }
}
C#

実装B:メール通知(イメージ)

class MailNotifier : INotifier
{
    public void Notify(string message)
    {
        // 実際のメール送信処理は省略
        Console.WriteLine($"[Mail] メールで通知: {message}");
    }
}
C#

依存を減らしたサービスクラス

class AlertService
{
    private readonly INotifier notifier;

    public AlertService(INotifier notifier)
    {
        this.notifier = notifier;
    }

    public void SendAlert(string text)
    {
        notifier.Notify($"アラート: {text}");
    }
}
C#

使う側

class Program
{
    static void Main(string[] args)
    {
        // コンソール通知版
        INotifier consoleNotifier = new ConsoleNotifier();
        AlertService consoleAlert = new AlertService(consoleNotifier);
        consoleAlert.SendAlert("コンソールに出すアラートです。");

        // メール通知版
        INotifier mailNotifier = new MailNotifier();
        AlertService mailAlert = new AlertService(mailNotifier);
        mailAlert.SendAlert("メールで送るアラートです。");

        Console.ReadLine();
    }
}
C#

ここでも、

  • AlertService は「INotifier という役割」にだけ依存している
  • 実際にどう通知するかは、外から渡される実装クラスに任せている

という疎結合な構造になっています。

Day 46 のまとめ

Day 46 では、

  • クラス同士の依存 → あるクラスが、別のクラスに強く頼りすぎている状態
  • 疎結合 → クラス同士の結びつきをゆるくし、役割(インターフェース)に依存する状態
  • 依存を減らす考え方 → 具体クラスではなく、インターフェースや抽象クラスに依存するように設計すること

を、ログ出力や通知の例を通して学びました。

依存関係を意識し始めると、

「このクラスは、何に依存しているんだろう?」 「この依存は、もっとゆるくできないかな?」

という視点が自然と生まれてきます。

それは、 コードを「動けばいい」から一歩進めて、

「長く育てていけるコード」にしていくための、とても大事な一歩

でもあります。

ぜひ、

  • 自分のコードの中で「具体クラスにべったり依存している場所」を探してみる
  • そこにインターフェースを挟んでみる
  • 実装を差し替える小さな実験をしてみる

という練習を通して、 依存関係と疎結合を 「設計のキーワード」ではなく、 自分のコードを軽やかにするための実践的な道具として、 少しずつ体に馴染ませていってください。

タイトルとURLをコピーしました