基礎から学ぶC#入門 90日コース | 実践力を高める - Day 69:設計の基本

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

Day 69 のゴールと全体像

Day 69 では、いよいよ 「設計の基本」 に踏み込んでいきます。 今日のキーワードはこの3つです。

  • 単一責任(Single Responsibility)
  • DRY(Don’t Repeat Yourself)
  • SOLID の概要

どれも、現場のプログラマーが「長く育てられるコード」を書くために、 意識し続けている大切な考え方です。

ここでは、難しい専門用語を“ふわっとしたイメージ”から入って、 少しずつコード例に落とし込みながら、 「設計ってこういうことか」と感じてもらえるように進めていきます。

単一責任の原則(Single Responsibility)

「ひとつのクラスは、ひとつのことに責任を持つ」

単一責任の原則は、

「ひとつのクラス(あるいはメソッド)は、 ひとつの“理由”でしか変更されないようにする」

という考え方です。

もう少しやわらかく言うと、

「このクラスは何を担当しているの?」 と聞かれたときに、 すっきり一言で答えられる状態にしておこう

というイメージです。

単一責任が守られていない例

まずは、よくある「何でも屋クラス」を見てみます。

using System;
using System.Collections.Generic;
using System.IO;

class UserManager
{
    private List<string> _users = new List<string>();

    public void AddUser(string name)
    {
        _users.Add(name);
    }

    public void PrintUsers()
    {
        Console.WriteLine("ユーザー一覧:");
        foreach (var u in _users)
        {
            Console.WriteLine($"- {u}");
        }
    }

    public void SaveToFile(string path)
    {
        // ファイルに保存する処理
        File.WriteAllLines(path, _users);
        Console.WriteLine("ファイルに保存しました。");
    }
}
C#

この UserManager は、

  • ユーザーの管理
  • コンソール表示
  • ファイル保存

と、3つの責任を抱え込んでしまっています。

その結果、

  • 表示方法を変えたいとき
  • 保存形式を変えたいとき
  • ユーザー管理のロジックを変えたいとき

など、いろいろな理由でこのクラスを変更しなければならず、 だんだん「触るのが怖いクラス」になっていきます。

単一責任に分割してみる

これを、責任ごとに分けてみます。

// ユーザーのデータを表すクラス
class User
{
    public string Name { get; set; }
}
C#
using System.Collections.Generic;

class UserRepository
{
    // ユーザーの一覧を管理する責任だけを持つ
    private readonly List<User> _users = new List<User>();

    public void Add(User user)
    {
        _users.Add(user);
    }

    public IEnumerable<User> GetAll()
    {
        return _users;
    }
}
C#
using System;
using System.Collections.Generic;

class UserConsoleView
{
    // コンソール表示だけを担当するクラス
    public void PrintUsers(IEnumerable<User> users)
    {
        Console.WriteLine("ユーザー一覧:");
        foreach (var u in users)
        {
            Console.WriteLine($"- {u.Name}");
        }
    }
}
C#
using System.Collections.Generic;
using System.IO;

class UserFileStorage
{
    // ファイル保存だけを担当するクラス
    public void Save(string path, IEnumerable<User> users)
    {
        var lines = new List<string>();
        foreach (var u in users)
        {
            lines.Add(u.Name);
        }

        File.WriteAllLines(path, lines);
    }
}
C#
// Program.cs
class Program
{
    static void Main()
    {
        var repo = new UserRepository();
        repo.Add(new User { Name = "Alice" });
        repo.Add(new User { Name = "Bob" });

        var view = new UserConsoleView();
        view.PrintUsers(repo.GetAll());

        var storage = new UserFileStorage();
        storage.Save("users.txt", repo.GetAll());
    }
}
C#

この状態になると、

  • 表示方法を変えたい → UserConsoleView だけ触ればいい
  • 保存形式を変えたい → UserFileStorage だけ触ればいい
  • ユーザー管理のロジックを変えたい → UserRepository だけ触ればいい

というふうに、 「変更の理由」がクラスごとに分かれていきます。

これが 単一責任の原則 のイメージです。

DRY 原則(Don’t Repeat Yourself)

「同じことを、何度も書かない」

DRY は、

「同じロジックを、コピペであちこちに書かないようにしよう」

という、とてもシンプルな原則です。

同じ処理があちこちに散らばっていると、

  • バグを直すときに、全部探して直さないといけない
  • どこか一箇所だけ直し忘れて、後で痛い目を見る

ということが起きやすくなります。

DRY が守られていない例

using System;

class Program
{
    static void Main()
    {
        int a = 10;
        int b = 20;

        // 合計を表示
        Console.WriteLine($"合計: {a + b}");

        int c = 5;
        int d = 7;

        // また合計を表示(同じロジックをコピペ)
        Console.WriteLine($"合計: {c + d}");

        int e = 3;
        int f = 9;

        // さらに合計を表示(またコピペ)
        Console.WriteLine($"合計: {e + f}");
    }
}
C#

ここでは、

  • 「2つの数値の合計を表示する」というロジックが
  • 何度もコピペされています。

もし「表示のフォーマットを変えたい」となったら、 3箇所すべてを探して直さないといけません。

DRY にするために共通化する

using System;

class Program
{
    static void Main()
    {
        PrintSum(10, 20);
        PrintSum(5, 7);
        PrintSum(3, 9);
    }

    // 共通のロジックとして切り出す
    static void PrintSum(int x, int y)
    {
        int sum = x + y;
        Console.WriteLine($"合計: {sum}");
    }
}
C#

このように、

  • 「同じことをしているコード」を見つけたら
  • 「共通のメソッドやクラス」に切り出す

というのが、DRY の基本的な実践方法です。

「あれ、これさっきも書いたな?」 と感じたら、DRY のチャンスだと思ってみてください。

SOLID の概要

SOLID は「設計の5つの指針」

SOLID は、

  • S:Single Responsibility Principle(単一責任の原則)
  • O:Open/Closed Principle(開放・閉鎖の原則)
  • L:Liskov Substitution Principle(リスコフの置換原則)
  • I:Interface Segregation Principle(インターフェイス分離の原則)
  • D:Dependency Inversion Principle(依存性逆転の原則)

の頭文字を取ったものです。

全部を一気に完璧に理解する必要はありませんが、 「こういう方向性で設計すると、コードが育てやすくなるんだな」 というざっくりしたイメージを持っておくと、 今後の学びがスムーズになります。

ここでは、初心者向けに、 ざっくりイメージ + ちょっとだけコード で紹介していきます。

S:Single Responsibility(単一責任の原則)

これは、さきほど詳しく見た 単一責任 です。

「ひとつのクラスは、ひとつのことに責任を持つ」

という考え方ですね。

O:Open/Closed(開放・閉鎖の原則)

「クラスは、拡張には開いていて、変更には閉じているべき」

という原則です。

やわらかく言うと、

「新しい機能を追加するときは、 できるだけ既存のクラスを直接書き換えずに、 拡張する形で対応しよう」

というイメージです。

変更に弱い例

class DiscountService
{
    public int GetPriceWithDiscount(int price, string userType)
    {
        if (userType == "Normal")
        {
            return price;
        }
        else if (userType == "Premium")
        {
            return (int)(price * 0.9);
        }
        else if (userType == "VIP")
        {
            return (int)(price * 0.8);
        }
        // 新しいユーザー種別が増えるたびに、ここを書き換える必要がある…
        return price;
    }
}
C#

拡張に強い形に近づける例

// 割引計算のインターフェイス
interface IDiscountPolicy
{
    int Apply(int price);
}

// 通常ユーザー
class NormalDiscount : IDiscountPolicy
{
    public int Apply(int price) => price;
}

// プレミアムユーザー
class PremiumDiscount : IDiscountPolicy
{
    public int Apply(int price) => (int)(price * 0.9);
}

// VIPユーザー
class VipDiscount : IDiscountPolicy
{
    public int Apply(int price) => (int)(price * 0.8);
}

class DiscountService
{
    public int GetPriceWithDiscount(int price, IDiscountPolicy policy)
    {
        return policy.Apply(price);
    }
}
C#

新しい割引ルールを追加したいときは、

  • 新しい IDiscountPolicy 実装クラスを作るだけ

で済み、DiscountService 自体は変更しなくてよくなります。

L:Liskov Substitution(リスコフの置換原則)

「サブクラスは、スーパークラスとして扱っても問題なく動くべき」

という原則です。

やわらかく言うと、

「親クラスの代わりに子クラスを使っても、 期待通りに振る舞ってくれないと困るよね」

という話です。

ここではイメージだけで十分です。 継承を使うときに「親として扱っても大丈夫かな?」と 少し意識してみると、感覚が育っていきます。

I:Interface Segregation(インターフェイス分離の原則)

「クラスに、使わないメソッドを押し付けるような 大きすぎるインターフェイスを渡さないようにしよう」

という原則です。

やわらかく言うと、

「インターフェイスは、 そのクラスが本当に必要としているメソッドだけに絞ろう」

というイメージです。

D:Dependency Inversion(依存性逆転の原則)

「具体的なクラスではなく、抽象(インターフェイス)に依存しよう」

という原則です。

やわらかく言うと、

new でガチガチに具体クラスを作るのではなく、 インターフェイスを通して柔らかくつないでおこう」

というイメージです。

さきほどの IDiscountPolicy の例は、 まさにこの「依存性逆転」の方向に近づいています。

Day 69 用ミニテンプレート

単一責任 + DRY を意識した小さな設計例

// データの形
class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}

// データの管理
class ProductRepository
{
    private readonly List<Product> _products = new List<Product>();

    public void Add(Product product) => _products.Add(product);

    public IEnumerable<Product> GetAll() => _products;
}

// 表示ロジック(DRY:表示の仕方をここに集約)
class ProductConsoleView
{
    public void PrintProducts(IEnumerable<Product> products)
    {
        Console.WriteLine("商品一覧:");
        foreach (var p in products)
        {
            Console.WriteLine($"- {p.Name} ({p.Price}円)");
        }
    }
}

// Program.cs
class Program
{
    static void Main()
    {
        var repo = new ProductRepository();
        repo.Add(new Product { Name = "ノートPC", Price = 120000 });
        repo.Add(new Product { Name = "マウス", Price = 2000 });

        var view = new ProductConsoleView();
        view.PrintProducts(repo.GetAll());
    }
}
C#

この小さな例の中にも、

  • 単一責任(役割ごとにクラスを分ける)
  • DRY(表示ロジックをひとつの場所にまとめる)

という設計の基本が、しっかり息づいています。

Day 69 のまとめ

Day 69 では、

  • 単一責任:ひとつのクラスは、ひとつのことに責任を持つ
  • DRY:同じロジックをコピペせず、共通化していく
  • SOLID:設計の方向性を示す5つの原則(今日はざっくり概要)

を、コード例とともに見てきました。

設計の原則は、 「覚えた瞬間に完璧に守れるもの」ではなく、 コードを書いて、直して、また書いて…というサイクルの中で、 少しずつ体に染み込んでいくものだと思います。

だからこそ、

  • 「このクラスは何を担当しているんだろう?」と自分に問いかけてみる
  • 「あれ、これさっきも書いたな?」と感じたら DRY を意識してみる
  • 「既存コードをガッツリ書き換えずに、拡張で対応できないかな?」と考えてみる

といった小さな意識を積み重ねていくことが、 設計力を育てるいちばんの近道になります。

ぜひ、Day 69 をきっかけに、 コードを「ただ動くもの」から 「長く育てていける相棒」へと 少しずつ育てていっていただければうれしいです。

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