はじめに
株式会社パレットリンクの@t-yonefuです。
最近、教育する立場になる機会が多く、「こう書いたら綺麗にコード書けるよ」、「最初の段階で上手く設計しないと後が大変だよ」などと助言することが増えました。
結局のところは経験則という部分が大半を占めますがこういった「どうコードを書いたら上手くいくか」というコーディング思想には大抵は名前がついていて、今回はそれらの一部を紹介していこうと思います。
本記事では、クラス設計の代表的な指針である SOLID原則 と GRASP の2つを取り上げます。それぞれ別の人が考えた用語ではありますが、それぞれに共通点、関連があるので一緒に覚えておくと実務での設計の判断がしやすくなります。
目次
SOLID原則
GRASP
- GRASPとは
- Information Expert(情報エキスパート)
- Creator(クリエイター)
- Low Coupling / High Cohesion(疎結合・高凝集)
- Controller(コントローラー)
- Polymorphism(多態性)
- Pure Fabrication(純粋造形)
- Indirection(間接化)
- Protected Variations(保護的変容)
- まとめ
SOLID原則とは
SOLID原則とは、保守性・拡張性・テスト容易性の高いソフトウェアを設計するための5つの原則です。
2000年代初頭に Robert C. Martin によってまとめられました。
頭文字を並べると「SOLID(堅固な)」という英単語になることから、この名前で広く知られています。
S - 単一責任の原則
Single Responsibility Principle(SRP)
「クラスを変更する理由は、たった1つでなければならない」
悪い例
ユーザー情報の取得と保存とメール送信が1つのクラスに混在しています。この場合、DBの仕様変更やメール送信先の変更のたびに同じクラスを触ることになるので修正による影響箇所が増えてしまう状態です。
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public User GetUser(int userId)
{
// ユーザー取得
return null;
}
public void SaveToDb(User user)
{
// DB保存 ← ユーザーと関係ない
}
public void SendEmail(User user)
{
// メール送信 ← これも関係ない
}
}
良い例
「ユーザーというデータ」「永続化」「メール送信」をそれぞれ別クラスに分けます。どれか1つを変えたいときも、該当するクラスだけを直せばよくなります。
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class UserRepository
{
public User Get(int userId) { return null; }
public void Save(User user) { }
}
public class EmailService
{
public void Send(User user) { }
}
ポイント
- 1つのクラスには1つの役割・責任だけを持たせる
- クラスが肥大化してきたら分割のサイン
- 変更理由が1つであれば、修正の影響範囲が最小限になる
O - 開放閉鎖の原則
Open/Closed Principle(OCP)
「クラスは拡張に対して寛容(Open)であり、修正に対して厳格(Closed)でなければならない」
悪い例
会員種別ごとの割引が、1つのメソッド内の分岐で書かれています。
割引の種類が増えるたびに、既存の if 分岐を書き換える設計だと、修正のたびにバグを入れやすくなります。
public class Discount
{
public decimal Apply(string userType, decimal price)
{
if (userType == "normal")
return price;
else if (userType == "member")
return price * 0.9m;
else if (userType == "vip")
// 新しい種類を追加するたびに既存コードを修正
return price * 0.8m;
return price;
}
}
良い例
割引ルールをインターフェースで切り出し、種類ごとにクラスを追加します。既存の NormalDiscount や MemberDiscount には手を入れず、SuperVipDiscount のような新しいクラスを足すだけで拡張できます。
追加は新しいクラスで、既存コードは触らない形にすると安全です。
public interface IDiscountStrategy
{
decimal Apply(decimal price);
}
public class NormalDiscount : IDiscountStrategy
{
public decimal Apply(decimal price) => price;
}
public class MemberDiscount : IDiscountStrategy
{
public decimal Apply(decimal price) => price * 0.9m;
}
public class VipDiscount : IDiscountStrategy
{
public decimal Apply(decimal price) => price * 0.8m;
}
// 新しい割引を追加しても既存コードは変更不要!
public class SuperVipDiscount : IDiscountStrategy
{
public decimal Apply(decimal price) => price * 0.7m;
}
ポイント
- 新機能追加は新しいクラス・モジュールの追加で対応する
- 既存のコードをできるだけ触らない
- インターフェースや抽象クラスを活用する
L - リスコフの置換原則
Liskov Substitution Principle(LSP)
「派生クラスは、基底クラスと置き換え可能でなければならない」
コンピュータ科学者 Barbara Liskov が1987年に提唱した原則です。
悪い例
「鳥」を継承した「ペンギン」に、親が持つ「飛ぶ」をそのまま継承させると、ペンギンは飛べないため例外を投げる実装になっています。そうすると「鳥型ならどこでも飛べる」という呼び出し側の前提が崩れ、置き換え可能ではなくなります。「Bird の変数に Penguin を入れても安全」という前提が成り立たない状態です。
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("飛んでいます");
}
}
public class Penguin : Bird
{
public override void Fly()
{
// 親クラスの期待する動作を壊している
throw new Exception("ペンギンは飛べません!");
}
}
良い例
「飛ぶ」「泳ぐ」ではなく、「動く」という抽象的な振る舞いにしておき、サブクラスごとに実装を変えます。ペンギンは「泳ぐ」、空を飛ぶ鳥は「飛ぶ」と実装すれば、どれも「Bird」として置き換えて使えます。
public abstract class Bird
{
public abstract void Move();
}
public class FlyingBird : Bird
{
public override void Move() => Console.WriteLine("飛んでいます");
}
public class Penguin : Bird
{
public override void Move() => Console.WriteLine("泳いでいます");
}
ポイント
- 継承関係は「is-a」の関係が正しいか再確認する
- 子クラスが親クラスの前提を壊してはいけない
I - インターフェース分離の原則
Interface Segregation Principle(ISP)
「クライアントは、使用しないメソッドへの依存を強制されるべきではない」
悪い例
IBird に Fly() と Swim() の両方があるため、オウムは泳げないのに Swim() を実装し、ペンギンは飛べないのに Fly() を実装し、それぞれで例外を投げるようになっているため余計な実装です。
public interface IBird
{
void Fly();
void Swim(); // ペンギンは飛べないし、オウムは泳げない
}
public class Parrot : IBird
{
public void Fly() => Console.WriteLine("飛んでいます");
public void Swim() => throw new Exception("オウムは泳げません(たぶん)");
// 不要なメソッドを実装させられている
}
public class Penguin : IBird
{
public void Fly() => throw new Exception("ペンギンは飛べません");
// 不要なメソッドを実装させられている
public void Swim() => Console.WriteLine("泳いでいます");
}
良い例
「飛べる」「泳げる」を別インターフェースに分け、オウムは IFlyable だけ、ペンギンは ISwimmable だけを実装します。使わないメソッドを無理に実装する必要がなくなります。使う機能を小さなインターフェースに分けると、無理のない実装になります。
public interface IFlyable
{
void Fly();
}
public interface ISwimmable
{
void Swim();
}
public class Parrot : IFlyable
{
public void Fly() => Console.WriteLine("飛んでいます");
// Swimは不要なので実装しなくてよい
}
public class Penguin : ISwimmable
{
public void Swim() => Console.WriteLine("泳いでいます");
// Flyは不要なので実装しなくてよい
}
ポイント
- インターフェースは小さく・具体的に分割する
- 「太った(fat)インターフェース」は避ける
- クライアントが必要なメソッドだけに依存できる設計にする
D - 依存性逆転の原則
Dependency Inversion Principle(DIP)
「上位モジュールは下位モジュールに依存してはならない。どちらも抽象に依存すべきである」
DI(依存性注入)はこの原則を実現するための具体的な手法です。
悪い例
UserService が MySqlDatabase という具体クラスを内部で new しており、DB の種類を変えるにはこのクラスを修正する必要があります。
public class MySqlDatabase
{
public User Find(int userId)
{
// MySQL固有の処理
return null;
}
}
public class UserService
{
private readonly MySqlDatabase _db;
public UserService()
{
_db = new MySqlDatabase();
// 具体クラスに直接依存
// Mysql→Postgresの移行のような変更に弱い
}
public User GetUser(int userId) => _db.Find(userId);
}
良い例
IDatabase という抽象に依存し、UserService のコンストラクタで実装を渡します。本番では MySqlDatabase、テストではモックを渡すといった切り替えが、既存コードの修正なしでできます。「どのDBか」は抽象(インターフェース)に依存し、具体的な実装は外から渡す形にすると、変更やテストが楽になります。
// 抽象(インターフェース)を定義
public interface IDatabase
{
User Find(int userId);
}
// 具体クラスは抽象を実装
public class MySqlDatabase : IDatabase
{
public User Find(int userId) { return null; }
}
public class PostgreSqlDatabase : IDatabase
{
public User Find(int userId) { return null; }
}
// 上位モジュールは抽象に依存
public class UserService
{
private readonly IDatabase _db;
// コンストラクタ注入(DI)
public UserService(IDatabase db) // 抽象に依存
{
_db = db;
}
public User GetUser(int userId) => _db.Find(userId);
}
// 外部から注入(DI)
var service = new UserService(new MySqlDatabase()); // 本番
// var service = new UserService(new MockDatabase()); // テスト時はモックを注入
ポイント
- 具体クラスではなくインターフェース・抽象クラスに依存する
- 依存オブジェクトは外部から注入(DI) する
- テスト時にモックへの差し替えが容易になる
GRASPとは
GRASP(General Responsibility Assignment Software Patterns)は、「どのオブジェクトにどの責任を割り当てるか」 を決めるためのパターン集です。
Craig Larman が『Applying UML and Patterns』で提唱しました。
SOLIDが「良い設計の性質」を表すのに対し、GRASPは 「責任をどこに置くか」という判断の指針 を具体的に示してくれます。クラスを分割したあと、「この処理はどのクラスに書くべきか?」で迷ったときに役立ちます。
本記事では、『実践UML パターンによる統一プロセスガイド(Craig Larman 著 / 依田 光江 訳)』に記載のある9つのパターンを紹介します。
Information Expert(情報エキスパート)
「責任は、その責任を果たすのに必要な情報を持っているオブジェクトに割り当てる」
ある処理に必要なデータを持っているクラスに、その処理を任せるという考え方です。
悪い例
注文の合計を計算するのに、注文明細を持っていない OrderService が order.GetItems() で中身を取り出して計算しています。情報の所在と計算の責任が一致していません。
// 注文の合計金額を計算するのに、Order ではなく別のクラスが Item の一覧にアクセスしている
public class OrderService
{
public decimal GetTotalPrice(Order order)
{
decimal total = 0;
foreach (var item in order.GetItems()) // Order が持っている情報を外から集めている
{
total += item.Price * item.Quantity;
}
return total;
}
}
良い例
「合計金額を計算するなら、注文内容(明細一覧)を持っているのは誰か?」を考えると、責任を置く場所が見えてきます。明細一覧(_items)を持っている Order に、そのデータを使って合計を出す GetTotalPrice() を置くと良いでしょう。情報とその振る舞いをまとめることが重要です。
// 注文(Order)が自分のアイテム一覧を持っている → 合計計算の責任も Order に
public class Order
{
private readonly List<OrderItem> _items = new();
public void AddItem(OrderItem item) => _items.Add(item);
public decimal GetTotalPrice()
{
return _items.Sum(item => item.Price * item.Quantity);
}
}
ポイント
- データを持っているオブジェクトに、そのデータを使う振る舞いをまとめる
- 情報が分散しないので、変更時の影響範囲がわかりやすい
Creator(クリエイター)
「オブジェクトを new する役割は、それを「中に含んでいる」オブジェクト、その一覧や履歴を持っているオブジェクト、それを頻繁に利用するオブジェクトのいずれかに担当させる」
「誰がそのインスタンスを作るのが自然か」を決めるパターンです。
悪い例
注文に属する OrderLine なのに、関係の薄い SomeService で new OrderLine() しています。どこで・どんな条件で明細が作られるかが分散し、一貫性を保ちにくくなります。
// 注文を作る場所がバラバラ。Order と OrderLine の関係が薄い場所で Line を作っている
public class OrderLine { }
public class SomeService
{
public void DoSomething()
{
var line = new OrderLine(); // Order の一部なのに、ここで作っている
// ...
}
}
良い例
「OrderLine は Order に属するもの」なら、OrderLine を new するのは Order の責務にすると、関係がはっきりします。OrderLine を保持している Order に、AddLine(...) で OrderLine を new してリストに追加する責任を持たせると明細の生成が Order に集約され、整合した状態を保ちやすくなります。
// Order が OrderLine を「持つ」ので、Line の作成責任は Order に
public class Order
{
private readonly List<OrderLine> _lines = new();
public OrderLine AddLine(string productId, int quantity, decimal price)
{
var line = new OrderLine(productId, quantity, price);
_lines.Add(line);
return line;
}
}
ポイント
- 包含関係やデータの所在を意識してオブジェクトを作ると、より明確な設計となる
- 生成ロジックが1か所にまとまり、一貫した状態を保ちやすい
Low Coupling / High Cohesion(疎結合・高凝集)
Low Coupling: クラス間の依存は少なく保つ
High Cohesion: 1つのクラスの中では、関連の強い責任だけをまとめる
SOLIDのSRPとも重なりますが、GRASPでは「責任の割り当て」の結果としての結合度・凝集度を意識します。
※2パターンありますが関連が深いので1セクションにまとめています
悪い例
DB・メール・ログの具体クラスを直接 new しているため、UserService がそれらに強く張り付いています。どれか1つを差し替えたりテスト用にモックしたりするだけでも、このクラスの修正が必要になります。
// 結合が高い:UserService が DB・メール・ログの具体クラスに直接依存
public class UserService
{
private readonly SqlUserRepository _repo = new();
private readonly SmtpEmailSender _email = new();
private readonly FileLogger _logger = new();
public void Register(User user)
{
_repo.Save(user);
_email.Send(user.Email, "メールだよ");
_logger.Write("User registered: " + user.Id);
}
}
良い例
結合度が高いと、1つの変更が多くのクラスに波及します。凝集度が低いと、1つのクラスに無関係な責任が混ざり、読みにくく変更しづらくなります。ここでは「登録」という一連の流れを担当するクラスが、どの程度ほかのクラスに依存するかを見ます。
インターフェース(IUserRepository など)にだけ依存し、実装はコンストラクタで受け取る形にします。UserService は「登録というユースケースをまとめる」という1つの役割に集中し、具体的なDBやメールの種類には依存しません。
// 抽象に依存して結合を下げる。UserService は「登録の流れ」に集中(高凝集)
public class UserService
{
private readonly IUserRepository _repo;
private readonly IEmailSender _email;
private readonly ILogger _logger;
public UserService(IUserRepository repo, IEmailSender email, ILogger logger)
{
_repo = repo;
_email = email;
_logger = logger;
}
public void Register(User user)
{
_repo.Save(user);
_email.Send(user.Email, "メールだよ");
_logger.Log("User registered: " + user.Id);
}
}
ポイント
- Low Coupling … 依存するクラス・型が少ないほど、変更の影響が広がりにくい
- High Cohesion … そのクラスが持つ責任が「ひとまとまりの意味」になっていると理解しやすい
- 個々の機能を明確にし、要素同士の依存度を下げることで保守性や再利用性を高める
Controller(コントローラー)
「システムイベント(ユースケース)の受け取りは、UI 以外の“コントローラー”に割り当てる」
画面やAPIの入力をそのままビジネスロジックに書かず、「ユースケースを司るクラス」に一度受けさせるパターンです。GRASPでいうコントローラーは、MVCのControllerやアプリケーションサービス(ユースケース層)が担う役割に対応します。
悪い例
フォームのイベントハンドラの中に、ユーザー生成・バリデーション・保存・メール送信まで全部書かれています。画面とドメイン・インフラが密結合し、同じ処理を別のUIから呼び出せません。
// フォームやAPIのハンドラに直接ビジネスロジックが書かれている
public class UserRegistrationForm
{
public void OnSubmitButtonClicked()
{
var user = new User(GetName(), GetEmail());
if (string.IsNullOrEmpty(user.Email)) throw new Exception("Invalid");
var repo = new SqlUserRepository();
repo.Save(user);
new SmtpEmailSender().Send(user.Email, "メールだよ");
// 画面とドメイン・インフラが密結合
}
}
良い例
「ユーザー登録」というユースケース専用の RegisterUserController を用意し、フォームは入力の取得とこのコントローラーの Execute 呼び出しだけにします。フォームの「送信」やAPIの「リクエスト」を受け取ったあと、「何をするか」の手順(ユースケース)はUIの外のクラスに任せると、登録の流れが1か所にまとまり、同じ登録処理をWeb・API・CLIから使い回せます。
// ユースケース「ユーザー登録」を担当するコントローラー(アプリケーションサービス)
public class RegisterUserController
{
private readonly IUserRepository _repo;
private readonly IEmailSender _email;
public RegisterUserController(IUserRepository repo, IEmailSender email)
{
_repo = repo;
_email = email;
}
public void Execute(string name, string email)
{
var user = new User(name, email);
_repo.Save(user);
_email.Send(user.Email, "メールだよ");
}
}
// 画面やAPIは「入力の受け取り」と「コントローラーの呼び出し」だけ
public class UserRegistrationForm
{
private readonly RegisterUserController _controller;
public void OnSubmitButtonClicked()
{
_controller.Execute(GetName(), GetEmail());
}
}
ポイント
- UI は入力と表示に専念し、何をするかはコントローラーに任せる
- 同じユースケースを別のUI(API・CLIなど)からも呼び出しやすくなる
Polymorphism(多態性)
「型によって振る舞いが変わる処理は、ポリモーフィズム(継承・インターフェース)に任せる」
if/switch で型を判定する代わりに、各型に振る舞いを持たせて共通のインターフェースで扱うパターンです。SOLIDのOCPと相性が良いです。
悪い例
支払い種別の文字列で if 分岐し、それぞれの処理を同じクラス内の private メソッドで持っています。新しい支払い方法を足すたびに、このクラスを編集することになります。
public class PaymentProcessor
{
public void Process(string paymentType, decimal amount)
{
if (paymentType == "CreditCard")
ProcessCreditCard(amount);
else if (paymentType == "PayPal")
ProcessPayPal(amount);
else if (paymentType == "BankTransfer")
ProcessBankTransfer(amount);
// 支払い方法が増えるたびにここを修正
}
private void ProcessCreditCard(decimal amount) { /* ... */ }
private void ProcessPayPal(decimal amount) { /* ... */ }
private void ProcessBankTransfer(decimal amount) { /* ... */ }
}
良い例
支払い方法ごとにクラスを1つずつ用意し、すべて IPaymentMethod で共通化します。PaymentProcessor は「渡された支払い方法で処理する」だけを担当し、新しい支払い方法は新しいクラスを追加するだけで対応できます。
同じクラス内の分岐が増えていく設計だと、修正箇所が集中してバグが発生しやすくなってしまうため、種類ごとにクラスを分け、共通のインターフェースで扱うと拡張が簡単になります。
public interface IPaymentMethod
{
void Process(decimal amount);
}
public class CreditCardPayment : IPaymentMethod
{
public void Process(decimal amount) { /* クレジット決済 */ }
}
public class PayPalPayment : IPaymentMethod
{
public void Process(decimal amount) { /* PayPal決済 */ }
}
public class BankTransferPayment : IPaymentMethod
{
public void Process(decimal amount) { /* 銀行振込決済 */ }
}
public class PaymentProcessor
{
public void Process(IPaymentMethod method, decimal amount)
{
method.Process(amount); // 型に応じた振る舞いは各クラスに任せる
}
}
ポイント
- 分岐を増やすのではなく、新しいクラスを追加して拡張する
- SOLIDのOCPと同じ考え方
Pure Fabrication(純粋造形)
「問題領域(ドメイン)上の自然な概念ではないが、責任をまとめるためにあえて特定機能だけを持つクラスを導入する」
Low Coupling / High Cohesion を守るために、あえてドメインモデルとは別の「サービス」クラスなどを作るパターンです。
悪い例
User が自分の Save() で SQL を発行しており、「ユーザーという概念」と「永続化の手段」が同じクラスに混在しています。DBを変えるだけで User を書き換える必要が出てきます。
// User が「ユーザー情報」と「永続化の詳細」の両方を知ってしまっている
public class User
{
public string Name { get; }
public string Email { get; }
public void Save()
{
using var connection = new SqlConnection("...");
// SQL処理を諸々
}
}
良い例
User は名前やメールといったドメインの属性だけを持ち、永続化は 「ドメインにはないが責任をまとめるために作った」UserRepository に任せます。
「ユーザー」は現実の概念ですが、「ユーザー情報をDBに保存する」という責任をユーザー自身に持たせると、ドメインオブジェクトがインフラの詳細(SQLなど)に縛られてしまいます。そのため、ドメインには存在しない「リポジトリ」のようなクラスをあえて作り、永続化の責任をそこに寄せています。
// User はドメインの概念に集中し、DB操作は「純粋な作り物」としてのリポジトリに任せる
public class User
{
public string Name { get; }
public string Email { get; }
}
public class UserRepository // Pure Fabrication
{
public void Save(User user)
{
using var connection = new SqlConnection("...");
// SQL処理を諸々
}
}
ポイント
- 「現実世界に対応する概念」だけにこだわらず、責任をうまくまとめるためのクラスを導入してよい
- 結果として Low Coupling / High Cohesion を達成しやすくなる
Indirection(間接化)
「仲介役(インターミディエイト)を挟んで、2つの要素の直接的な結合を弱める」
直接依存させず、間に1つレイヤーを挟むことで、変更の影響を小さくするパターンです。
悪い例
画面のイベントハンドラが直接 SqlUserRepository を new して保存しています。UI がインフラに依存しており、登録の手順を変えたりリポジトリを差し替えたりするときに、画面のコードを触ることになります。
// UI が直接リポジトリに依存し、ビジネスロジックも混ざっている
public class UserPage
{
public void OnSaveButtonClicked(string name, string email)
{
var repo = new SqlUserRepository();
var user = new User(name, email);
repo.Save(user);
}
}
良い例
画面(UI)がいきなりリポジトリを呼ぶと、UIの変更やリポジトリの差し替えが互いに影響し合います。「ユースケースを実行する層」を間に挟むと、UIはその層だけに依存し、永続化の詳細から切り離せます。
UserApplicationService(ユースケース層)を間に挟み、画面は「名前とメールを渡して Register を呼ぶ」だけにすることで、画面はリポジトリを知らず、リポジトリの実装を変えても画面の変更は不要にできます。
// ApplicationService(ユースケース層)を間に挟んで間接化する
public class UserApplicationService
{
private readonly IUserRepository _repo;
public UserApplicationService(IUserRepository repo)
{
_repo = repo;
}
public void Register(string name, string email)
{
var user = new User(name, email);
_repo.Save(user);
}
}
public class UserPage
{
private readonly UserApplicationService _service;
public void OnSaveButtonClicked(string name, string email)
{
_service.Register(name, email); // UI はユースケースにだけ依存
}
}
ポイント
- 間に 1 つレイヤー(サービス、アダプタ、ファサードなど)を挟み、依存関係を整理する
- 層ごと・役割ごとの責任範囲が明確になる
- 要素同士の依存を減らす(疎結合化)ために繋ぎのための層を用意する
Protected Variations(保護的変容)
「変化しやすい部分を、安定したインターフェースの裏に隠す」
将来変わりそうな実装(DB・外部API・ライブラリなど)に直接依存せず、抽象の向こう側に隠すパターンです。DIPやOCPと重なります。
悪い例
特定の決済サービス(Stripe)のクラスを直接 new しており、別の決済手段に変える・テストでモックに差し替えるには、このクラスの中身を書き換える必要があります。
// 特定の決済サービスに直接依存。サービスが変わると修正だらけに
public class OrderService
{
public void Charge(Order order)
{
var stripe = new StripeClient("xxxxx");
stripe.Charge(order.TotalAmount, order.CustomerCardToken);
}
}
良い例
決済を「Stripe のクラスを直接使う」と書いてしまうと、別の決済サービスに切り替えたり、テストで決済をスキップしたりするたびに、注文まわりのコードを直す必要が出てきます。「決済する」というインターフェースの向こうに実装を隠すと、変更やテストがしやすくなります。
OrderService は IPaymentGateway というインターフェースにだけ依存し、実際の決済(Stripe / PayPal / 自社システムなど)はコンストラクタで渡す形にします。本番・テスト・将来の差し替えで、既存の注文ロジックには手を入れずに済みます。
// 決済は「インターフェース」に依存。実装(Stripe / PayPal / 独自)は差し替え可能
public interface IPaymentGateway
{
void Charge(decimal amount, string customerToken);
}
public class OrderService
{
private readonly IPaymentGateway _payment;
public OrderService(IPaymentGateway payment)
{
_payment = payment;
}
public void Charge(Order order)
{
_payment.Charge(order.TotalAmount, order.CustomerPaymentToken);
}
}
ポイント
- 変化しやすい部分をインターフェースや抽象クラスの向こうに置く
- 実装の差し替え・テスト時のモック化がしやすくなる
まとめ
SOLID原則
| 原則 | 一言まとめ |
|---|---|
| S - 単一責任 | 1クラス1責任 |
| O - 開放閉鎖 | 追加は寛容に、修正は注意して |
| L - リスコフ置換 | 子は親の代わりになれる |
| I - インターフェース分離 | 使わないものに依存しない |
| D - 依存性逆転 | 具体でなく抽象に依存する |
GRASP(責任の割り当て)
| パターン | 一言まとめ |
|---|---|
| Information Expert | 必要な情報を持っているオブジェクトに責任を割り当てる |
| Creator | 集約・包含する側が、その一部のオブジェクトを作る |
| Low Coupling / High Cohesion | 依存は少なく、クラス内の責任は関連の強いものだけに |
| Controller | ユースケースの受け取りはUIではなくコントローラーに |
| Polymorphism | 型による分岐はポリモーフィズムで表現する |
| Pure Fabrication | ドメインとは離れた別のクラスにあえて責任を集約する |
| Indirection | 仲介役を挟んで直接の依存を弱める |
| Protected Variations | 変化しやすい部分をインターフェースの裏に隠す |
SOLID と GRASP
- SOLID … 「どんな設計が望ましいか」の原則(1クラス1責任、抽象に依存する、など)
- GRASP … 「この処理はどのクラスに書くか?」の責任の所在を決めるためのパターン(情報を持っているクラスに任せる、作成は集約ルートに、など)
両方を意識すると、クラスをどう分けるかと責任をどこに置くかの判断が揃い、変更に強く読みやすいクラス設計に近づけます。
さいごに
これは極論ではありますが、AIを使えばクラス設計なんて気にしないでコードが書けてしまいます。エンジニアが気にするのは機能と渡された仕様が一致しているかだけになり、
どう書いているかはすべてAI任せになってしまうでしょう。ただ私が思うに、「しなくていいこと」と「知らなくていいこと」は必ずしも一致しません。
AIが勝手にやってくれること、ライブラリやツールがやってくれることを知らずに作業をすることはとても恐ろしいことです。そして何よりも理解しないでする作業はまったく面白くありません。
良いエンジニアになるために正しく理解して設計・開発を楽しみましょう。
パレットリンクでは、日々のつながりや学びを大切にしながら、さまざまなお役立ち記事をお届けしています。よろしければ、ぜひ「Organization」のページもご覧ください。
また、私たちと一緒に未来をつくっていく仲間も募集中です。ご興味をお持ちの方は、ぜひお気軽にお問い合わせください。一緒に新しいご縁が生まれることを楽しみにしています。