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 をきっかけに、 コードを「ただ動くもの」から 「長く育てていける相棒」へと 少しずつ育てていっていただければうれしいです。
