Null Objectパターンは、null参照による条件分岐や例外発生を回避するための構造的デザインパターンです。特に、オブジェクトが存在しない状況を「無意味な値」ではなく「振る舞いを持つデフォルト実装」として表現することで、クライアントコードの堅牢性と可読性を向上させます。
問題の背景
従来の実装では、サービス層から返されるオブジェクトがnullである可能性に対し、呼び出し側で毎回nullチェックを行う必要があります。この重複した防御的コードは保守性を損ない、見落としがあればNullReferenceExceptionを引き起こします。
改良前のコード例(リスクあり)
エンティティ(脆弱な実装):
public class Learner
{
public int Identifier { get; set; }
public string FullName { get; set; }
public void AttendLecture() => Console.WriteLine($"{FullName} is attending a lecture.");
}サービス(nullを返す):
public class LearnerService
{
public Learner FetchById(int id) => id switch
{
101 => new Learner { Identifier = 101, FullName = "Yamada" },
_ => null // 問題の根源
};
}クライアント(危険な呼び出し):
var service = new LearnerService();
var learner = service.FetchById(999);
Console.WriteLine($"Name: {learner.FullName}"); // NullReferenceException!Null Objectパターンによる解決
まず、共通の振る舞いを定義する抽象基底クラスを導入します:
public abstract class Participant
{
public int Identifier { get; set; }
public virtual string FullName => "Anonymous";
public abstract void AttendLecture();
}次に、実際のデータを持つクラスと、空状態を安全に表現するクラスをそれぞれ派生させます:
public class RegisteredLearner : Participant
{
public override string FullName { get; set; } = "Unknown";
public override void AttendLecture() =>
Console.WriteLine($"{FullName} is attending a lecture.");
}
public class AbsentParticipant : Participant
{
public AbsentParticipant()
{
Identifier = -1;
}
public override void AttendLecture() =>
Console.WriteLine("No participant available — operation skipped.");
}サービス層(常に非nullを保証):
public class ParticipantService
{
public Participant Retrieve(int id) => id switch
{
101 => new RegisteredLearner { Identifier = 101, FullName = "Yamada" },
_ => new AbsentParticipant() // 常に有効なインスタンスを返す
};
}クライアント(シンプルかつ安全):
var service = new ParticipantService();
var participant = service.Retrieve(999); // 必ずParticipantのインスタンス
Console.WriteLine($"Name: {participant.FullName}");
participant.AttendLecture(); // 安全に呼び出せる実行結果は以下の通りです:
Name: Anonymous
No participant available — operation skipped.
適用上の考慮事項
このパターンは、ドメインモデルに「存在しない」状態が自然に解釈できるシナリオ(例:未登録ユーザー、無効な設定、代替不可なデフォルト)で特に有効です。一方、空オブジェクトが業務ロジック上「無意味」な場合や、エラー状態を明示的に伝達すべき場合は、代わりにResult<T>型や例外処理が適切です。また、すべてのエンティティに専用のNullクラスを用意すると、型の爆発を招く可能性があるため、抽象化粒度の検討が必要です。