Phantom Type (幽霊型)っていうのを知った。 インスタンスの状態をメンバフィールドではなく型パラメータで持つことで、 状態チェックをコンパイル時に行えるテクニック。 なにそれ凄い。
型パラメータに指定するだけで、 インスタンス化したりメソッド呼んだりして実際に使わないから、 Phantom (幽霊) なんだそうな。 中二な名前嫌いじゃない。
C# で Phantom Type を実践できるか試してみた。 特定の状態でのみ呼べるメソッドを定義するのに拡張メソッドを使ったのと、 コンストラクタを internal にして拡張メソッドからは呼べるようにしたあたりに、 苦労の跡が見える。 アセンブリの外からはコンストラクタ呼べないから、まぁいいかな、と。 Scala だともっとスマートに書けるみたいだけど。
出来上がったのは Phantom Type もどきだな。
using System; namespace PhantomTypeSampleLib { // 状態を表すインタフェース public interface IStatus { } // 下書き public class Draft : IStatus { private Draft() { } } // 公開済み public class Published : IStatus { private Published() { } } // 状態遷移するメソッドを拡張メソッドで定義しておいて、 // 特定の状態でしか呼べないようにした。 // ファクトリメソッドも定義。 public static class Article { // Article<T> のインスタンスを作成する唯一のメソッド public static Article<Draft> CreateDraft() { return new Article<Draft>( title: "", body: "", publishedAt: null); } public static Article<Published> Publish(this Article<Draft> article) { return new Article<Published>( title: article.Title, body: article.Body, publishedAt: DateTime.Today); } public static Article<Draft> UnPublish(this Article<Published> article) { return new Article<Draft>( title: article.Title, body: article.Body, publishedAt: null); } } public class Article<T> where T : IStatus { public DateTime? PublishedAt { get; private set; } public string Title { get; private set; } public string Body { get; private set; } // 拡張メソッドからアクセスできるようにしておく。 internal Article(string title, string body, DateTime? publishedAt) { Title = title; Body = body; PublishedAt = publishedAt; } // 状態遷移しない操作を定義 public Article<T> SetTitle(string newTitle) { return new Article<T>(title: newTitle, body: Body, publishedAt: PublishedAt); } public Article<T> SetBody(string newBody) { return new Article<T>(title: Title, body: newBody, publishedAt: PublishedAt); } } }
// PhantomTypeSampleLib とは別のプロジェクト using PhantomTypeSampleLib; using System; namespace PhantomTypeSample { class Program { static void Main(string[] args) { var article = Article.CreateDraft() .SetTitle("foo") .SetBody("bar") .Publish(); // Publish は Article<Draft> でしか呼べない Console.WriteLine($"Title:{article.Title}, Body:{article.Body}, PublishedAt:{article.PublishedAt}"); Console.WriteLine("Enter で終了します。"); Console.ReadLine(); } } }