C#クラス設計の基本と実践|初心者でもわかる責務分離・継承・インターフェースの考え方

はじめに

C#でプログラムを書き始めると、最初は「動くコード」を作ることに意識が向きがちです。しかし、開発が進むにつれて「どこに何を書けばよいかわからない」「修正したら別の場所が壊れた」「同じような処理が増えて管理しづらい」といった問題が出てきます。

そこで重要になるのが、C#のクラス設計です。クラス設計とは、単にクラスを作ることではなく、データや処理の役割を整理し、変更しやすく、読みやすく、テストしやすいコードにするための考え方です。

この記事では、C#クラス設計の基本から、責務分離、継承、インターフェース、SOLID原則、実践的な改善手順までを初心者にもわかりやすく解説します。

1. C#クラス設計とは?初心者が最初に理解すべき基本

1-1. クラス設計の目的は「変更しやすいコード」を作ること

C#クラス設計の目的は、きれいなコードを書くことだけではありません。最も大切なのは、あとから変更しやすいコードを作ることです。

実務では、一度書いたコードを何度も修正します。仕様変更、新機能追加、不具合修正、画面変更、データ保存方法の変更など、コードは常に変化します。そのとき、クラス設計が悪いと、少しの修正でも多くのファイルに影響が広がります。

良いクラス設計では、役割ごとに処理が分かれているため、変更範囲を小さくできます。たとえば、ユーザー情報を保存する処理を変更したい場合、保存処理を担当するクラスだけを修正すれば済むように設計します。

つまり、C#クラス設計は「今動けばよいコード」ではなく、「将来の変更に耐えられるコード」を作るための土台です。

1-2. クラス・オブジェクト・インスタンスの違い

C#のクラス設計を理解するには、クラス、オブジェクト、インスタンスの違いを押さえておく必要があります。

クラスは、データや処理の設計図です。たとえば、Userクラスを作ると、ユーザーが持つ名前、メールアドレス、ログイン処理などを定義できます。

C#
public class User
{
public string Name { get; set; }
public string Email { get; set; }

public void Login()
{
Console.WriteLine($"{Name}がログインしました");
}
}

インスタンスは、そのクラスから実際に作られた実体です。

C#
var user = new User();
user.Name = "Taro";
user.Email = "taro@example.com";

オブジェクトは、インスタンスとほぼ同じ意味で使われることが多く、プログラム上で扱う実体を指します。

初心者のうちは、「クラスは設計図」「インスタンスは設計図から作った実物」と考えると理解しやすいです。

1-3. フィールド・プロパティ・メソッドの役割

C#のクラスは、主にフィールド、プロパティ、メソッドで構成されます。

フィールドは、クラス内部で保持するデータです。基本的にはprivateにして、外部から直接変更できないようにします。

C#
private int age;

プロパティは、外部から安全に値を取得・設定するための仕組みです。

C#
public string Name { get; private set; }

メソッドは、クラスが行う処理や振る舞いを表します。

C#
public void ChangeName(string newName)
{
if (string.IsNullOrWhiteSpace(newName))
{
throw new ArgumentException("名前は必須です");
}

Name = newName;
}

良いC#クラス設計では、フィールドで状態を持ち、プロパティで安全に公開し、メソッドで意味のある操作を提供します。単にデータを入れるだけでなく、「このクラスは何を担当するのか」を意識して構成することが大切です。

1-4. 良いクラス設計と悪いクラス設計の違い

良いクラス設計は、役割が明確です。クラス名を見ただけで何をするクラスなのかがわかり、メソッドもその役割に沿っています。

たとえば、UserRepositoryという名前なら、ユーザーデータの保存や取得を担当するクラスだと予想できます。UserValidatorなら、ユーザー情報の検証を担当するクラスだとわかります。

一方、悪いクラス設計では、1つのクラスに多くの責務が詰め込まれます。

C#
public class UserManager
{
public void RegisterUser() { }
public void ValidateEmail() { }
public void SaveToDatabase() { }
public void SendWelcomeMail() { }
public void DisplayUserInfo() { }
}

このようなクラスは一見便利ですが、登録、検証、保存、メール送信、表示という複数の責務を持っています。そのため、どこを修正すればよいか分かりにくく、変更の影響も大きくなります。

良い設計とは、複雑な処理を1つのクラスに押し込めることではなく、役割ごとに適切に分けることです。

1-5. C#でクラス設計が重要になる場面

C#クラス設計が特に重要になるのは、プログラムの規模が大きくなる場面です。

コンソールアプリの小さな練習では、1つのファイルに処理を書いても問題になりにくいです。しかし、Webアプリ、業務システム、Unityゲーム、Windowsアプリなどでは、多くの機能が関係します。

たとえば、Webアプリでは、ユーザー登録、認証、データベース保存、入力チェック、メール送信、画面表示などの処理があります。これらを適切にクラス分けしないと、機能追加や修正が難しくなります。

C#はオブジェクト指向の機能が豊富な言語です。クラス、継承、インターフェース、アクセス修飾子、プロパティなどを正しく使うことで、保守性の高い設計を実現できます。

2. C#クラス設計で押さえるべき基本ルール

2-1. クラス名は役割がわかる名前にする

C#クラス設計では、クラス名が非常に重要です。クラス名は、そのクラスの役割を表す看板のようなものです。

悪い例として、次のような名前があります。

C#
public class DataManager
{
}

DataManagerという名前は抽象的すぎます。何のデータを管理するのか、どのような処理をするのかがわかりません。

より具体的にするなら、次のようにします。

C#
public class UserRepository
{
}

public class OrderService
{
}

public class EmailSender
{
}

UserRepositoryはユーザーデータの保存や取得、OrderServiceは注文に関する業務処理、EmailSenderはメール送信を担当すると想像できます。

クラス名には、役割を表す名詞を使います。Service、Repository、Validator、Factory、Controllerなどの名前はよく使われますが、何でも付ければよいわけではありません。名前を見て責務が伝わるかを意識しましょう。

2-2. 1つのクラスに複数の責務を持たせない

C#クラス設計の基本は、1つのクラスに1つの主な責務を持たせることです。責務とは、そのクラスが担当する仕事のことです。

たとえば、ユーザー登録処理を考えると、次のような責務があります。

入力値を検証する責務、ユーザー情報を保存する責務、登録完了メールを送る責務、登録結果を表示する責務です。

これらをすべてUserクラスに入れると、クラスが大きくなりすぎます。変更理由も増えます。メール文面を変えたいだけなのにUserクラスを修正する、保存先を変更したいだけなのにUserクラスを修正する、といった状態になります。

責務を分けることで、それぞれのクラスが小さくなり、修正しやすくなります。

2-3. public・private・protectedの使い分け

C#では、アクセス修飾子を使って、クラスやメンバーの公開範囲を制御できます。

publicは、どこからでもアクセスできます。外部から使ってほしいメソッドやプロパティに使います。

privateは、そのクラスの内部からだけアクセスできます。外部に公開する必要のないフィールドや補助メソッドに使います。

protectedは、そのクラス自身と派生クラスからアクセスできます。継承を使う場合に、派生クラスから使わせたい処理に利用します。

C#
public class User
{
private string password;

public string Name { get; private set; }

protected void SetName(string name)
{
Name = name;
}
}

初心者が意識すべき基本は、「迷ったらprivateにする」ことです。最初から何でもpublicにすると、外部から自由に変更されてしまい、クラスの状態を安全に保てません。

2-4. プロパティで状態を安全に管理する

C#では、フィールドを直接publicにするのではなく、プロパティを使って状態を管理します。

悪い例は次のようなコードです。

C#
public class User
{
public int Age;
}

この場合、外部からAgeにどんな値でも入れられます。年齢にマイナス値を設定することもできてしまいます。

プロパティとメソッドを使えば、不正な状態を防げます。

C#
public class User
{
public int Age { get; private set; }

public void ChangeAge(int age)
{
if (age < 0)
{
throw new ArgumentException("年齢は0以上である必要があります");
}

Age = age;
}
}

このように、状態を変更する入口を制限することで、クラス内部の整合性を守れます。C#クラス設計では、データをただ公開するのではなく、安全に扱える形で公開することが重要です。

2-5. コンストラクタで初期化の責任を明確にする

コンストラクタは、インスタンスを作るときに必要な初期化を行う特別なメソッドです。

C#
public class User
{
public string Name { get; }
public string Email { get; }

public User(string name, string email)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です");
}

if (string.IsNullOrWhiteSpace(email))
{
throw new ArgumentException("メールアドレスは必須です");
}

Name = name;
Email = email;
}
}

この設計では、Userインスタンスを作る時点でNameとEmailが必ず設定されます。作成後に「名前がないユーザー」や「メールアドレスがないユーザー」が存在しにくくなります。

コンストラクタを使うことで、クラスが成立するために必要な情報を明確にできます。初期化の責任を曖昧にしないことは、C#クラス設計の基本です。

3. 責務分離の考え方|クラスを分ける判断基準

3-1. 責務とは何か

責務とは、クラスが担当する役割や仕事のことです。

たとえば、Userクラスの責務は「ユーザー情報を表現すること」です。UserRepositoryの責務は「ユーザーデータを保存・取得すること」です。UserValidatorの責務は「ユーザー情報が正しいか検証すること」です。

責務を考えるときは、「このクラスは何のために存在するのか」と問いかけるとわかりやすくなります。答えが1つにまとまるなら良い設計に近いです。複数の答えが出てくるなら、責務が混ざっている可能性があります。

3-2. 単一責任の原則をC#で理解する

単一責任の原則とは、1つのクラスは1つの変更理由だけを持つべき、という考え方です。

たとえば、Userクラスにユーザー情報、入力チェック、データベース保存、メール送信をすべて入れると、変更理由が多くなります。

メール仕様が変わったらUserクラスを変更します。保存先が変わってもUserクラスを変更します。入力チェックルールが変わってもUserクラスを変更します。

これは単一責任の原則に反しています。

C#では、次のように責務ごとにクラスを分けます。

C#
public class User
{
public string Name { get; }
public string Email { get; }

public User(string name, string email)
{
Name = name;
Email = email;
}
}

public class UserValidator
{
public bool IsValid(User user)
{
return !string.IsNullOrWhiteSpace(user.Name)
&& !string.IsNullOrWhiteSpace(user.Email);
}
}

public class UserRepository
{
public void Save(User user)
{
// データベースに保存する処理
}
}

このように分けると、それぞれのクラスが何を担当しているのか明確になります。

3-3. 責務が多すぎるクラスの問題点

責務が多すぎるクラスには、いくつかの問題があります。

まず、コードが読みにくくなります。1つのクラスに多くのメソッドがあると、どこに何が書かれているのか探すだけで時間がかかります。

次に、変更の影響範囲が広がります。メール送信処理を修正しただけなのに、ユーザー登録処理や保存処理に影響するかもしれません。

さらに、テストしにくくなります。データベース接続、メール送信、入力チェックが1つのクラスに混ざっていると、単体テストを書くのが難しくなります。

巨大なクラスは、最初は便利に見えます。しかし、長期的には保守性を下げる原因になります。C#クラス設計では、クラスが大きくなり始めたら、責務が増えすぎていないか確認しましょう。

3-4. 処理・データ・表示を分ける考え方

責務分離を考えるときは、処理、データ、表示を分けると整理しやすくなります。

データを表すクラスは、UserやOrderなど、業務上の情報を保持します。

処理を担当するクラスは、UserServiceやOrderServiceなど、業務ロジックを実行します。

表示を担当する部分は、画面やコンソール出力、Web APIのレスポンスなどを扱います。

たとえば、Userクラスの中でConsole.WriteLineを直接呼び出すと、ユーザー情報と表示処理が結びついてしまいます。コンソールアプリでは問題に見えなくても、WebアプリやUnityに移したときに使い回しにくくなります。

表示方法が変わってもデータや業務処理に影響しないように、役割を分けることが重要です。

3-5. 責務分離の具体例:Userクラスを改善する

改善前のUserクラスを見てみましょう。

C#
public class User
{
public string Name { get; set; }
public string Email { get; set; }

public bool Validate()
{
return !string.IsNullOrWhiteSpace(Name)
&& !string.IsNullOrWhiteSpace(Email);
}

public void Save()
{
// データベースに保存
}

public void SendMail()
{
// メール送信
}
}

このUserクラスは、ユーザー情報の保持、検証、保存、メール送信を担当しています。責務が多すぎます。

改善後は、次のように分けられます。

C#
public class User
{
public string Name { get; }
public string Email { get; }

public User(string name, string email)
{
Name = name;
Email = email;
}
}

public class UserValidator
{
public bool IsValid(User user)
{
return !string.IsNullOrWhiteSpace(user.Name)
&& !string.IsNullOrWhiteSpace(user.Email);
}
}

public class UserRepository
{
public void Save(User user)
{
// データベースに保存
}
}

public class UserMailService
{
public void SendWelcomeMail(User user)
{
// メール送信
}
}

クラス数は増えますが、それぞれの役割は明確になります。変更が必要になったときも、どのクラスを修正すべきか判断しやすくなります。

3-6. クラスを分けすぎる失敗例にも注意する

責務分離は重要ですが、クラスを分けすぎるのも問題です。

たとえば、1行の処理しかないクラスを大量に作ると、処理の流れを追いにくくなります。小さすぎるクラスが増えると、ファイルを行き来する手間が増え、かえって理解しづらくなります。

クラスを分ける判断基準は、「変更理由が違うかどうか」です。変更される理由が同じなら、同じクラスにまとめても問題ありません。変更理由が明らかに違うなら、別クラスに分ける価値があります。

初心者のうちは、最初から完璧に分けようとする必要はありません。まずは動くコードを書き、クラスが大きくなってきたら責務を見直す、という流れで十分です。

4. 継承の基本|共通処理を再利用する設計

4-1. C#における継承とは

継承とは、あるクラスの性質や処理を別のクラスが引き継ぐ仕組みです。共通する処理を基底クラスにまとめ、個別の違いを派生クラスで実装できます。

C#
public class Animal
{
public void Eat()
{
Console.WriteLine("食べます");
}
}

public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("吠えます");
}
}

DogクラスはAnimalクラスを継承しているため、Eatメソッドを使えます。

C#
var dog = new Dog();
dog.Eat();
dog.Bark();

C#クラス設計における継承は、共通処理を再利用するための強力な仕組みです。ただし、使い方を間違えると変更しにくい設計になるため注意が必要です。

4-2. baseクラスと派生クラスの関係

継承元のクラスを基底クラス、継承先のクラスを派生クラスと呼びます。

基底クラスには共通する処理を書き、派生クラスには個別の処理を書きます。

C#
public class Employee
{
public string Name { get; }

public Employee(string name)
{
Name = name;
}

public void Work()
{
Console.WriteLine($"{Name}が働きます");
}
}

public class Engineer : Employee
{
public Engineer(string name) : base(name)
{
}

public void WriteCode()
{
Console.WriteLine($"{Name}がコードを書きます");
}
}

baseは、基底クラスのコンストラクタやメソッドを呼び出すために使います。この例では、EngineerのコンストラクタからEmployeeのコンストラクタを呼び出しています。

基底クラスと派生クラスは、「派生クラスは基底クラスの一種である」と自然に言える関係で使うのが基本です。

4-3. virtual・overrideの使い方

C#では、基底クラスのメソッドを派生クラスで上書きしたい場合、virtualとoverrideを使います。

C#
public class Payment
{
public virtual void Pay()
{
Console.WriteLine("支払いを行います");
}
}

public class CreditCardPayment : Payment
{
public override void Pay()
{
Console.WriteLine("クレジットカードで支払います");
}
}

基底クラスでvirtualを付けると、派生クラスでoverrideできます。

C#
Payment payment = new CreditCardPayment();
payment.Pay();

このコードでは、変数の型はPaymentですが、実際のインスタンスはCreditCardPaymentなので、CreditCardPaymentのPayメソッドが実行されます。

これはポリモーフィズムと呼ばれる考え方で、C#クラス設計において柔軟な処理切り替えを実現できます。

4-4. abstractクラスを使うべき場面

abstractクラスは、直接インスタンス化できない基底クラスです。共通処理を持ちながら、一部の処理は派生クラスに実装させたい場合に使います。

C#
public abstract class Report
{
public void Print()
{
Console.WriteLine(CreateContent());
}

protected abstract string CreateContent();
}

public class SalesReport : Report
{
protected override string CreateContent()
{
return "売上レポート";
}
}

Reportクラスは、Printという共通処理を持っています。しかし、レポート内容の作り方は派生クラスごとに異なるため、CreateContentをabstractにしています。

abstractクラスは、「共通の流れは決まっているが、一部の具体的な処理は派生クラスに任せたい」ときに便利です。

4-5. 継承でやってはいけない設計

継承でよくある失敗は、「コードを再利用したいだけ」で継承を使うことです。

たとえば、Printerクラスのメソッドを使いたいからといって、ReportクラスがPrinterを継承するのは不自然です。

C#
public class Printer
{
public void Print()
{
}
}

public class Report : Printer
{
}

ReportはPrinterの一種ではありません。このような継承は、クラス同士の関係をわかりにくくします。

継承を使うときは、「AはBの一種である」と言えるかを確認しましょう。言えない場合は、継承ではなくコンポジションを検討します。

4-6. 継承よりコンポジションを選ぶべきケース

コンポジションとは、クラスの中に別のクラスを持たせて機能を利用する設計です。

C#
public class Printer
{
public void Print(string text)
{
Console.WriteLine(text);
}
}

public class Report
{
private readonly Printer printer;

public Report(Printer printer)
{
this.printer = printer;
}

public void PrintReport()
{
printer.Print("レポート内容");
}
}

この設計では、ReportはPrinterを継承していません。代わりに、Printerを部品として利用しています。

継承は親子関係を固定するため、あとから変更しにくくなることがあります。一方、コンポジションは部品を差し替えやすく、柔軟な設計に向いています。

C#クラス設計では、まずコンポジションで考え、明確な親子関係がある場合に継承を使うと失敗しにくくなります。

5. インターフェースの基本|振る舞いを抽象化する設計

5-1. C#のinterfaceとは

C#のinterfaceは、クラスが持つべき振る舞いを定義する仕組みです。具体的な処理内容ではなく、「何ができるか」を表します。

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

このインターフェースは、メール送信ができることを表しています。実際の送信方法は、実装クラスに任せます。

C#
public class SmtpEmailSender : IEmailSender
{
public void Send(string to, string subject, string body)
{
// SMTPでメールを送信
}
}

interfaceを使うと、利用側は具体的なクラスではなく、振る舞いに依存できます。これにより、実装の差し替えがしやすくなります。

5-2. インターフェースを使うメリット

インターフェースを使う主なメリットは、実装を差し替えやすくなることです。

たとえば、メール送信処理で本番環境ではSmtpEmailSenderを使い、テストではFakeEmailSenderを使うことができます。

C#
public class FakeEmailSender : IEmailSender
{
public void Send(string to, string subject, string body)
{
// テスト用なので実際には送信しない
}
}

利用側のクラスはIEmailSenderに依存していれば、具体的な送信方法を知る必要がありません。

C#
public class UserService
{
private readonly IEmailSender emailSender;

public UserService(IEmailSender emailSender)
{
this.emailSender = emailSender;
}

public void Register(User user)
{
emailSender.Send(user.Email, "登録完了", "登録ありがとうございます");
}
}

この設計により、テストしやすく、変更に強いコードになります。

5-3. 実装クラスと利用側を分離する考え方

C#クラス設計では、利用側が具体的なクラスを直接newしすぎると、依存関係が強くなります。

C#
public class UserService
{
private readonly SmtpEmailSender emailSender = new SmtpEmailSender();
}

この場合、UserServiceはSmtpEmailSenderに強く依存しています。別のメール送信方法に変えたい場合、UserServiceを修正しなければなりません。

インターフェースを使うと、利用側と実装クラスを分離できます。

C#
public class UserService
{
private readonly IEmailSender emailSender;

public UserService(IEmailSender emailSender)
{
this.emailSender = emailSender;
}
}

この形にすると、UserServiceは「メールを送れるもの」に依存しているだけです。具体的にSMTPなのか、APIなのか、テスト用なのかは外部から渡せます。

この考え方は、依存性注入とも関係します。ASP.NET CoreなどのC#開発では特によく使われる設計です。

5-4. インターフェースと抽象クラスの違い

インターフェースと抽象クラスは、どちらも抽象化に使えますが、目的が異なります。

インターフェースは、「何ができるか」という振る舞いを定義します。複数のクラスに共通の能力を持たせたい場合に向いています。

抽象クラスは、「共通の状態や処理を持つ基底クラス」として使います。共通処理を提供しつつ、一部の処理を派生クラスに任せたい場合に向いています。

たとえば、IPrintableは印刷できることを表すインターフェースに向いています。一方、ReportBaseはレポート共通の処理を持つ抽象クラスに向いています。

初心者は、共通処理を持たせたいなら抽象クラス、振る舞いだけを約束したいならインターフェース、と考えると理解しやすいです。

5-5. すべてのクラスにインターフェースは必要か

C#クラス設計では、インターフェースは便利ですが、すべてのクラスに作る必要はありません。

実装が1つしかなく、差し替える予定もなく、テストでも困らない場合、無理にインターフェースを作ると設計が複雑になります。

たとえば、単純な値を表すUserやProductのようなクラスに、必ずIUserやIProductを作る必要はありません。

インターフェースを作るべき場面は、実装を差し替えたいとき、外部サービスやデータベースなどに依存するとき、テスト用の実装を使いたいとき、複数の実装クラスを同じように扱いたいときです。

重要なのは、インターフェースを作ること自体ではなく、依存関係を整理することです。

5-6. インターフェース設計の具体例

支払い処理を例に考えてみます。クレジットカード、銀行振込、電子マネーなど、支払い方法は複数あります。

まず、支払いの振る舞いをインターフェースで定義します。

C#
public interface IPaymentMethod
{
void Pay(decimal amount);
}

次に、具体的な支払い方法を実装します。

C#
public class CreditCardPayment : IPaymentMethod
{
public void Pay(decimal amount)
{
Console.WriteLine($"{amount}円をクレジットカードで支払いました");
}
}

public class BankTransferPayment : IPaymentMethod
{
public void Pay(decimal amount)
{
Console.WriteLine($"{amount}円を銀行振込で支払いました");
}
}

利用側は、IPaymentMethodに依存します。

C#
public class OrderService
{
private readonly IPaymentMethod paymentMethod;

public OrderService(IPaymentMethod paymentMethod)
{
this.paymentMethod = paymentMethod;
}

public void Checkout(decimal amount)
{
paymentMethod.Pay(amount);
}
}

この設計なら、新しい支払い方法を追加してもOrderServiceを大きく変更せずに済みます。C#クラス設計でインターフェースを使うと、拡張しやすい構造を作れます。

6. C#クラス設計で知っておきたいSOLID原則

6-1. SOLID原則とは

SOLID原則とは、オブジェクト指向設計で保守性や拡張性を高めるための5つの設計原則です。

C#クラス設計においても非常に重要な考え方で、クラスの責務、拡張方法、継承の使い方、インターフェース設計、依存関係の整理に役立ちます。

SOLIDは、次の5つの原則の頭文字です。

SRPは単一責任の原則、OCPは開放閉鎖の原則、LSPはリスコフの置換原則、ISPはインターフェース分離の原則、DIPは依存関係逆転の原則です。

初心者が最初からすべてを完璧に理解する必要はありません。まずは「責務を分ける」「変更に強くする」「具体的な実装に依存しすぎない」という視点で学ぶとよいでしょう。

6-2. SRP:単一責任の原則

SRPは、1つのクラスは1つの責務だけを持つべきという原則です。

たとえば、Userクラスがユーザー情報の保持、入力チェック、保存、メール送信をすべて担当している場合、SRPに反しています。

SRPを意識すると、次のように分けられます。

Userはユーザー情報を表す、UserValidatorは入力チェックをする、UserRepositoryは保存する、EmailSenderはメールを送る、という形です。

SRPは、C#クラス設計の中でも初心者が最初に意識すべき原則です。クラスが大きくなってきたら、「このクラスの変更理由は何個あるか」と考えてみましょう。

6-3. OCP:開放閉鎖の原則

OCPは、拡張に対して開いていて、修正に対して閉じているべきという原則です。

簡単に言うと、新しい機能を追加するときに、既存コードをなるべく変更せずに済む設計を目指すということです。

たとえば、支払い方法をif文で分岐しているコードは、新しい支払い方法を追加するたびに既存メソッドを修正する必要があります。

C#
public void Pay(string type)
{
if (type == "credit")
{
// クレジットカード処理
}
else if (type == "bank")
{
// 銀行振込処理
}
}

インターフェースを使えば、新しい支払い方法をクラスとして追加できます。

C#
public interface IPaymentMethod
{
void Pay(decimal amount);
}

既存コードを大きく変更せず、実装クラスを追加することで拡張できるようになります。

6-4. LSP:リスコフの置換原則

LSPは、派生クラスは基底クラスとして扱っても正しく動作すべきという原則です。

たとえば、BirdクラスにFlyメソッドを持たせ、PenguinクラスがBirdを継承すると問題が起きます。ペンギンは鳥ですが、飛べません。

C#
public class Bird
{
public virtual void Fly()
{
}
}

public class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException();
}
}

この設計では、Birdとして扱ったときにFlyできると期待してしまいます。しかし、Penguinでは例外が発生します。

このような場合は、FlyをBirdに持たせるのではなく、IFlyableのようなインターフェースに分ける方が自然です。

LSPは、継承関係が本当に正しいかを確認するための原則です。

6-5. ISP:インターフェース分離の原則

ISPは、クラスが使わないメソッドに依存しないように、インターフェースを適切に分けるべきという原則です。

悪い例として、大きすぎるインターフェースがあります。

C#
public interface IWorker
{
void Work();
void Eat();
}

人間のWorkerならEatできますが、ロボットのWorkerはEatしません。その場合、RobotWorkerに不要なEatメソッドを実装させることになります。

改善するなら、次のように分けます。

C#
public interface IWorkable
{
void Work();
}

public interface IEatable
{
void Eat();
}

必要な振る舞いだけに依存できるため、実装クラスが無理なメソッドを持たなくて済みます。

6-6. DIP:依存関係逆転の原則

DIPは、上位のクラスが具体的な実装ではなく、抽象に依存すべきという原則です。

たとえば、UserServiceがSmtpEmailSenderを直接newしていると、具体的な実装に依存しています。

C#
public class UserService
{
private readonly SmtpEmailSender emailSender = new SmtpEmailSender();
}

これをIEmailSenderに依存する形に変更します。

C#
public class UserService
{
private readonly IEmailSender emailSender;

public UserService(IEmailSender emailSender)
{
this.emailSender = emailSender;
}
}

この設計なら、SmtpEmailSender以外の実装にも差し替えられます。テスト用のFakeEmailSenderも使えます。

DIPは、C#のインターフェースや依存性注入と相性がよく、実務でも頻繁に使われます。

6-7. 初心者が最初に意識すべきSOLID原則

初心者が最初に意識すべきSOLID原則は、SRPとDIPです。

SRPを意識すると、クラスの責務を分ける習慣が身につきます。巨大なクラスを避け、読みやすいコードを書きやすくなります。

DIPを意識すると、具体的な実装に依存しすぎない設計を学べます。インターフェースの必要性も理解しやすくなります。

OCP、LSP、ISPも重要ですが、最初から完璧に適用しようとすると難しく感じます。まずは、1つのクラスに何でも入れないこと、外部サービスやデータベース処理はインターフェースで分離することから始めましょう。

7. 実践例で学ぶC#クラス設計の改善手順

7-1. 改善前のコード例

次のコードは、ユーザー登録を行うクラスです。

C#
public class UserManager
{
public void Register(string name, string email)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です");
}

if (string.IsNullOrWhiteSpace(email))
{
throw new ArgumentException("メールアドレスは必須です");
}

Console.WriteLine("データベースに保存しました");
Console.WriteLine($"{email}に登録完了メールを送信しました");
}
}

小さなサンプルとしては問題なく見えます。しかし、実務のC#クラス設計として考えると、複数の責務が混ざっています。

7-2. 問題点を洗い出す

このUserManagerには、主に4つの責務があります。

ユーザー情報を受け取る責務、入力値を検証する責務、データベースに保存する責務、登録完了メールを送信する責務です。

このままだと、メール送信方法を変更したい場合もUserManagerを修正します。保存先をデータベースからAPIに変える場合もUserManagerを修正します。入力チェックルールを追加する場合もUserManagerを修正します。

つまり、変更理由が多すぎます。これがC#クラス設計でよくある問題です。

7-3. 責務ごとにクラスを分割する

まず、ユーザー情報を表すUserクラスを作ります。

C#
public class User
{
public string Name { get; }
public string Email { get; }

public User(string name, string email)
{
Name = name;
Email = email;
}
}

次に、検証用のUserValidatorを作ります。

C#
public class UserValidator
{
public void Validate(User user)
{
if (string.IsNullOrWhiteSpace(user.Name))
{
throw new ArgumentException("名前は必須です");
}

if (string.IsNullOrWhiteSpace(user.Email))
{
throw new ArgumentException("メールアドレスは必須です");
}
}
}

さらに、保存処理とメール送信処理も分けます。

C#
public class UserRepository
{
public void Save(User user)
{
Console.WriteLine("データベースに保存しました");
}
}

public class EmailSender
{
public void Send(string email, string message)
{
Console.WriteLine($"{email}に{message}を送信しました");
}
}

7-4. 継承を使って共通処理を整理する

継承は、共通する処理が複数のクラスにある場合に使えます。

たとえば、複数の通知クラスでログ出力を共通化したい場合、基底クラスにまとめることができます。

C#
public abstract class NotificationSender
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}

public abstract void Send(string to, string message);
}

public class MailNotificationSender : NotificationSender
{
public override void Send(string to, string message)
{
Log("メール送信開始");
Console.WriteLine($"{to}にメールを送信しました");
}
}

ただし、継承は本当に親子関係が自然な場合に使います。共通処理を使いたいだけなら、コンポジションやインターフェースの方が適している場合もあります。

7-5. インターフェースで依存関係を整理する

次に、保存処理とメール送信処理をインターフェースで抽象化します。

C#
public interface IUserRepository
{
void Save(User user);
}

public interface IEmailSender
{
void Send(string email, string message);
}

実装クラスは、このインターフェースを実装します。

C#
public class UserRepository : IUserRepository
{
public void Save(User user)
{
Console.WriteLine("データベースに保存しました");
}
}

public class EmailSender : IEmailSender
{
public void Send(string email, string message)
{
Console.WriteLine($"{email}に{message}を送信しました");
}
}

これにより、利用側は具体的なUserRepositoryやEmailSenderではなく、IUserRepositoryやIEmailSenderに依存できます。

7-6. 改善後のコード例

改善後のUserServiceは次のようになります。

C#
public class UserService
{
private readonly UserValidator validator;
private readonly IUserRepository userRepository;
private readonly IEmailSender emailSender;

public UserService(
UserValidator validator,
IUserRepository userRepository,
IEmailSender emailSender)
{
this.validator = validator;
this.userRepository = userRepository;
this.emailSender = emailSender;
}

public void Register(string name, string email)
{
var user = new User(name, email);

validator.Validate(user);
userRepository.Save(user);
emailSender.Send(user.Email, "登録完了メール");
}
}

この設計では、UserServiceは登録処理の流れを管理します。検証、保存、メール送信の詳細は別クラスに任せています。

このように責務を分けると、保存方法を変えたい場合はIUserRepositoryの実装を変更すればよく、メール送信方法を変えたい場合はIEmailSenderの実装を変更すれば済みます。

7-7. 設計改善で読みやすくなるポイント

改善後のコードは、クラス数が増えています。しかし、それぞれの役割が明確です。

Userはデータを表す、UserValidatorは検証する、IUserRepositoryは保存処理を抽象化する、IEmailSenderはメール送信を抽象化する、UserServiceは登録処理の流れを管理する、という構造です。

C#クラス設計では、クラス数を少なくすることが目的ではありません。変更しやすく、読みやすく、テストしやすい状態にすることが目的です。

設計改善のポイントは、処理をただ分割することではなく、責務と依存関係を整理することです。

8. C#クラス設計でよくある失敗と対策

8-1. 何でも入った巨大クラスになる

初心者によくある失敗は、1つのクラスに何でも入れてしまうことです。

UserManager、GameManager、DataManagerのような名前のクラスは、責務が広がりやすい傾向があります。最初は便利でも、機能追加のたびにメソッドが増え、どこに何があるかわからなくなります。

対策は、クラスの変更理由を確認することです。入力チェック、保存、表示、通知など、変更理由が異なる処理は別クラスに分けることを検討しましょう。

8-2. 継承を使いすぎて変更しづらくなる

継承を使いすぎると、基底クラスの変更が多くの派生クラスに影響します。また、継承階層が深くなると、処理の流れを追いにくくなります。

対策は、継承を使う前に「本当に親子関係か」を確認することです。「AはBの一種である」と自然に言えない場合は、コンポジションを選ぶ方が安全です。

共通処理を再利用したいだけなら、別クラスに切り出して持たせる設計を検討しましょう。

8-3. インターフェースを細かく作りすぎる

インターフェースは便利ですが、細かく作りすぎると設計が複雑になります。

たとえば、実装が1つしかなく、差し替える予定もないクラスに毎回インターフェースを作ると、ファイル数が増え、理解しにくくなります。

対策は、インターフェースを作る目的を明確にすることです。テストで差し替えたい、外部サービスへの依存を分けたい、複数の実装を同じように扱いたい、といった理由がある場合に使いましょう。

8-4. privateにすべき情報をpublicにしてしまう

フィールドやプロパティを何でもpublicにすると、外部から自由に変更されてしまいます。その結果、クラスの状態が不正になる可能性があります。

たとえば、年齢や金額、在庫数などに不正な値が入ると、バグの原因になります。

対策は、外部から変更してよい情報だけをpublicにすることです。状態変更にはメソッドを用意し、その中で検証を行います。

C#
public int Stock { get; private set; }

public void ReduceStock(int quantity)
{
if (quantity <= 0)
{
throw new ArgumentException("数量は1以上である必要があります");
}

Stock -= quantity;
}

このように、状態を守る設計を意識しましょう。

8-5. 名前から役割が伝わらない

クラス名やメソッド名が曖昧だと、コードの意図が伝わりません。

Process、Execute、Manager、Helperなどの名前は便利ですが、多用すると役割が不明確になります。

対策は、何を処理するのか、何を管理するのか、何を補助するのかを具体的に名前に含めることです。

たとえば、DataHelperよりもCsvUserImporter、UserManagerよりもUserRegistrationServiceの方が役割が伝わります。

C#クラス設計では、名前も重要な設計要素です。良い名前は、コメントよりも強力に意図を伝えます。

8-6. テストしにくい設計になる

テストしにくい設計もよくある失敗です。

たとえば、クラス内部でデータベース接続や外部API呼び出しを直接newしていると、単体テストが難しくなります。

C#
public class OrderService
{
private readonly PaymentApiClient client = new PaymentApiClient();
}

この設計では、テスト時にPaymentApiClientを差し替えにくくなります。

対策は、インターフェースとコンストラクタ注入を使うことです。

C#
public class OrderService
{
private readonly IPaymentClient paymentClient;

public OrderService(IPaymentClient paymentClient)
{
this.paymentClient = paymentClient;
}
}

このようにすれば、テスト時にFakePaymentClientを渡せます。C#クラス設計では、テストしやすさも設計品質の重要な指標です。

9. 初心者がC#クラス設計を身につける学習ステップ

9-1. まずは小さなクラスを作って動かす

C#クラス設計を身につけるには、まず小さなクラスを作って動かすことが大切です。

最初から完璧な設計を目指す必要はありません。User、Product、Orderなど、身近なものをクラスにして、プロパティやメソッドを定義してみましょう。

たとえば、ProductクラスにName、Price、ChangePriceメソッドを作るだけでも、状態と振る舞いの関係を学べます。

コードを書きながら、「このデータはどのクラスが持つべきか」「この処理はどのクラスに置くべきか」を考える習慣をつけましょう。

9-2. 既存コードの責務を分けてみる

次のステップは、既存コードの責務を分ける練習です。

1つのクラスに入力チェック、計算、保存、表示が混ざっているコードを見つけ、それぞれを別クラスに分けてみましょう。

最初はクラス分けが多すぎたり、逆に分け方が足りなかったりするかもしれません。しかし、実際に手を動かすことで、責務分離の感覚が身につきます。

リファクタリング前後で、どちらが読みやすいか、変更しやすいかを比較することも効果的です。

9-3. 継承とインターフェースを使い分ける練習をする

継承とインターフェースは、C#クラス設計でよく使う重要な機能です。

継承は、「AはBの一種である」と言える関係に使います。たとえば、DogはAnimalの一種です。

インターフェースは、「何ができるか」を表すときに使います。たとえば、IPrintableは印刷できること、ISavableは保存できることを表します。

練習として、支払い方法、通知方法、ファイル出力方法などをインターフェースで設計してみると理解が深まります。

9-4. SOLID原則を1つずつ実践する

SOLID原則は、読むだけでは身につきません。1つずつ実際のコードに適用してみることが大切です。

まずはSRPを意識して、1つのクラスに複数の責務を持たせないようにします。

次にDIPを意識して、外部サービスやデータベース処理をインターフェースで分離してみます。

慣れてきたら、OCPを意識して、新機能追加時に既存コードの修正を減らす設計を考えます。

一度にすべてを完璧に守る必要はありません。設計に迷ったときの判断材料として、少しずつ使えるようになれば十分です。

9-5. リファクタリングで設計力を高める

設計力は、最初から正しいコードを書く力だけではありません。既存のコードをより良く改善する力も重要です。

リファクタリングでは、動作を変えずにコードの構造を改善します。長いメソッドを分割する、責務が多いクラスを分ける、重複コードを共通化する、具体クラスへの依存をインターフェースに変える、といった作業です。

C#クラス設計を学ぶうえで、リファクタリングは非常に効果的です。自分が過去に書いたコードを見直し、「今ならどう分けるか」を考えてみましょう。

9-6. 実務・Unity・Webアプリでの活用例を見る

C#クラス設計は、さまざまな場面で活用できます。

実務の業務システムでは、Service、Repository、Entity、Validatorなどに責務を分ける設計がよく使われます。

Unityでは、Player、Enemy、GameManager、InputHandler、ScoreServiceなど、ゲーム内の役割ごとにクラスを分けます。ただし、GameManagerに何でも入れすぎると巨大クラスになりやすいので注意が必要です。

Webアプリでは、Controller、Service、Repository、DTO、Modelなどに分けることで、画面、業務処理、データアクセスを整理できます。

実際のアプリでどのようにクラスが分けられているかを見ることで、C#クラス設計の理解はさらに深まります。

まとめ

C#クラス設計は、保守しやすく、変更に強く、読みやすいコードを書くために欠かせない考え方です。

初心者がまず意識すべきことは、クラスの責務を明確にすることです。1つのクラスに何でも入れるのではなく、データ、処理、保存、表示、通知などの役割を分けることで、コードは整理されます。

継承は、共通処理を再利用できる便利な仕組みですが、本当に親子関係がある場合に使うことが大切です。単なるコード共有のために継承を使うと、かえって変更しにくくなることがあります。

インターフェースは、振る舞いを抽象化し、実装クラスと利用側を分離するために役立ちます。特に外部サービス、データベース、メール送信、決済処理など、差し替えやテストが必要な処理では有効です。

また、SOLID原則を学ぶことで、C#クラス設計の判断基準が身につきます。最初はすべてを完璧に理解する必要はありません。まずは単一責任の原則と依存関係逆転の原則から意識してみましょう。

良い設計は、最初から完成するものではありません。コードを書き、問題点を見つけ、リファクタリングしながら少しずつ改善していくものです。C#クラス設計の基本を押さえ、責務分離、継承、インターフェースを適切に使い分けることで、実務でも通用する読みやすく変更しやすいコードを書けるようになります。