Skip to main content

SOLIDの原則

SOLIDの原則は、ソフトウェア設計をより理解しやすく、柔軟で、保守しやすいものにするための5つの設計原則の頭字語です。 .NET (C#) 開発においても、これらの原則を適用することで、堅牢なアプリケーションを構築することができます。

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

「クラスを変更する理由は、ただ一つであるべきである」

クラスは単一の機能や責任のみを持つべきです。複数の責任を持つクラスは、変更の影響範囲が広くなり、バグの原因になりやすくなります。

悪い例

UserService クラスが、ユーザーの登録処理と、登録完了メールの送信処理の両方を行っています。

public class UserService
{
public void Register(string email, string password)
{
// ユーザー登録ロジック
Console.WriteLine("User registered.");

// メール送信ロジック (責任が混在している)
Console.WriteLine($"Sending email to {email}...");
}
}

良い例

メール送信の責任を別のクラス (EmailService) に分離します。

public class EmailService
{
public void SendEmail(string email, string message)
{
Console.WriteLine($"Sending email to {email}: {message}");
}
}

public class UserService
{
private readonly EmailService _emailService;

public UserService(EmailService emailService)
{
_emailService = emailService;
}

public void Register(string email, string password)
{
// ユーザー登録ロジック
Console.WriteLine("User registered.");

// メール送信は専門のクラスに委譲
_emailService.SendEmail(email, "Welcome!");
}
}

O: 開放/閉鎖の原則 (Open/Closed Principle - OCP)

「ソフトウェアの構成要素(クラス、モジュール、関数など)は、拡張に対しては開いていて、修正に対しては閉じているべきである」

既存のコードを変更することなく、新しい機能を追加できるように設計すべきです。通常はインターフェースや抽象クラスを使用して実現します。

悪い例

新しい支払い方法を追加するたびに、PaymentProcessor クラスの if 文や switch 文を修正する必要があります。

public class PaymentProcessor
{
public void ProcessPayment(string type)
{
if (type == "CreditCard")
{
// クレジットカード決済
}
else if (type == "PayPal")
{
// PayPal決済
}
// 新しい支払い方法が増えるたびに修正が必要
}
}

良い例

インターフェースを定義し、各支払い方法はそれを実装します。PaymentProcessor は具体的な実装を知る必要がありません。

public interface IPaymentMethod
{
void Process();
}

public class CreditCardPayment : IPaymentMethod
{
public void Process() => Console.WriteLine("Processing Credit Card...");
}

public class PayPalPayment : IPaymentMethod
{
public void Process() => Console.WriteLine("Processing PayPal...");
}

public class PaymentProcessor
{
public void ProcessPayment(IPaymentMethod method)
{
// 新しい支払い方法が増えても、このコードは変更不要
method.Process();
}
}

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

「派生型は、その基本型と置換可能でなければならない」

サブクラスは、親クラスの振る舞いを壊してはいけません。親クラスのインスタンスをサブクラスのインスタンスに置き換えても、プログラムが正しく動作する必要があります。

悪い例

Square (正方形) を Rectangle (長方形) のサブクラスにすると、幅と高さを個別に設定できるという長方形の前提が崩れます。

public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
}

public class Square : Rectangle
{
public override int Width
{
set { base.Width = value; base.Height = value; }
}

public override int Height
{
set { base.Width = value; base.Height = value; }
}
}

// 問題点:
// Rectangle rect = new Square();
// rect.Width = 5;
// rect.Height = 10;
// 期待値: Area = 50, 実際: Area = 100 (正方形の制約によりWidthも10になるため)

良い例

共通のインターフェースや抽象クラスを使用し、継承関係を見直します。

public interface IShape
{
int Area { get; }
}

public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Area => Width * Height;
}

public class Square : IShape
{
public int SideLength { get; set; }
public int Area => SideLength * SideLength;
}

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

「クライアントが利用しないメソッドへの依存を強制してはならない」

一つの巨大なインターフェースを作るよりも、目的別の小さなインターフェースを複数作る方が良いです。

悪い例

IMultiFunctionPrinter インターフェースは、印刷、スキャン、FAXの全ての機能を強制します。単純なプリンターでも ScanFax を実装しなければなりません。

public interface IMultiFunctionPrinter
{
void Print();
void Scan();
void Fax();
}

public class SimplePrinter : IMultiFunctionPrinter
{
public void Print() { /* 印刷処理 */ }
public void Scan() { throw new NotImplementedException(); } // 不要
public void Fax() { throw new NotImplementedException(); } // 不要
}

良い例

インターフェースを機能ごとに分離します。

public interface IPrinter
{
void Print();
}

public interface IScanner
{
void Scan();
}

public class SimplePrinter : IPrinter
{
public void Print() { /* 印刷処理 */ }
}

public class MultiFunctionMachine : IPrinter, IScanner
{
public void Print() { /* 印刷処理 */ }
public void Scan() { /* スキャン処理 */ }
}

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

「上位モジュールは下位モジュールに依存してはならない。両者とも抽象に依存すべきである」 「抽象は詳細に依存してはならない。詳細は抽象に依存すべきである」

具体的なクラスに依存するのではなく、インターフェースや抽象クラスに依存することで、結合度を下げます。これは依存性注入 (Dependency Injection) の基礎となります。

悪い例

NotificationService (上位) が EmailSender (下位・詳細) に直接依存しています。SMS送信に変更したい場合、NotificationService の修正が必要です。

public class EmailSender
{
public void Send(string message) => Console.WriteLine("Email sent");
}

public class NotificationService
{
private EmailSender _sender = new EmailSender(); // 直接依存

public void Notify(string message)
{
_sender.Send(message);
}
}

良い例

IMessageSender インターフェース (抽象) に依存させます。

public interface IMessageSender
{
void Send(string message);
}

public class EmailSender : IMessageSender
{
public void Send(string message) => Console.WriteLine("Email sent");
}

public class SmsSender : IMessageSender
{
public void Send(string message) => Console.WriteLine("SMS sent");
}

public class NotificationService
{
private readonly IMessageSender _sender;

// コンストラクタインジェクション
public NotificationService(IMessageSender sender)
{
_sender = sender;
}

public void Notify(string message)
{
_sender.Send(message);
}
}