20年後のデザインパターン「状態」

状態は動作デザインパターンです。 プログラムの実行中に、オブジェクトの状態に応じてオブジェクトの動作を変更する必要がある場合に使用されます。 従来の実装では、可能な状態ごとにすべてのメソッドと1つのクラスを含む基本的な抽象クラスまたはインターフェイスを作成します。 このテンプレートは、「 条件演算子をポリモーフィズムに置き換える 」という推奨事項の特別なケースです。



すべてはこの本によると思われますが、ニュアンスがあります。 この状態に関係のないメソッドを実装する方法は? たとえば、空のバスケットからアイテムを削除したり、空のバスケットの代金を支払う方法は? 通常、各状態クラスは関連するメソッドのみを実装し、他の場合はInvalidOperationException



スローしInvalidOperationException







顔のリスコフを置換する原則の違反。 ヤロンミンスキーは代替アプローチを提案しました違法状態を代表できないようにしてください 。 これにより、ランタイムからコンパイル時にエラーチェックを転送できます。 ただし、この場合の制御フローは、多型を使用せずに、サンプルとの比較に基づいて編成されます。 幸いなことに、 パターンマッチングの部分的なサポートがC#7で登場しました



F#の例の詳細については、 Scott VlashinのWebサイトで 、違法な状態を代表できないようにするトピック公開されています


バスケットの例の「状態」の実装を検討してください。 C#にはユニオン型はありません。 データと動作を分離します。 状態自体は、enumと別のクラスの動作を使用してエンコードされます。 便宜上、enumと対応する動作クラス、「状態」の基本クラスをバインドする属性を宣言し、enumから動作クラスに移動する拡張メソッドを追加します。



インフラ



  [AttributeUsage(AttributeTargets.Field)] public class StateAttribute : Attribute { public Type StateType { get; } public StateAttribute(Type stateType) { StateType = stateType ?? throw new ArgumentNullException(nameof(stateType)); } } public abstract class State<T> where T: class { protected State(T entity) { Entity = entity ?? throw new ArgumentNullException(nameof(entity)); } protected T Entity { get; } } public static class StateCodeExtensions { public static State<T> ToState<T>(this Enum stateCode, object entity) where T : class // ,  reflection .   expression tree //  IL Emit    => (State<T>) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute<StateAttribute>() .StateType, entity); }
      
      





サブジェクトエリア



エンティティ「バスケット」を宣言します。



 public interface IHasState<TStateCode, TEntity> where TEntity : class { TStateCode StateCode { get; } State<TEntity> State { get; } } public partial class Cart : IHasState<Cart.CartStateCode, Cart> { public User User { get; protected set; } public CartStateCode StateCode { get; protected set; } public State<Cart> State => StateCode.ToState<Cart>(this); public decimal Total { get; protected set; } protected virtual ICollection<Product> Products { get; set; } = new List<Product>(); // ORM Only protected Cart() { } public Cart(User user) { User = user ?? throw new ArgumentNullException(nameof(user)); StateCode = StateCode = CartStateCode.Empty; } public Cart(User user, IEnumerable<Product> products) : this(user) { StateCode = StateCode = CartStateCode.Empty; foreach (var product in products) { Products.Add(product); } } public Cart(User user, IEnumerable<Product> products, decimal total) : this(user, products) { if (total <= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
      
      





バスケットの状態ごとに1つのクラスを実装します:empty、active、payed、しかし、共通のインターフェースは宣言しません。 各状態に関連する動作のみを実現させます。 これは、クラスEmptyCartState



ActiveCartState



、およびPaidCartState



が同じインターフェースを実装できないことを意味するものではありません。 それらは可能ですが、そのようなインターフェースには、各状態で利用可能なメソッドのみを含める必要があります。 この場合、 Add



メソッドはEmptyCartState



およびActiveCartState



で使用できるため、抽象AddableCartStateBase



からそれらを継承できます。 ただし、商品は未払いのバスケットにのみ追加できるため、すべての州に共通のインターフェースはありません。 したがって、コンパイル段階でコードにInvalidOperationException



が存在しないことを保証します。



  public partial class Cart { public enum CartStateCode: byte { [State(typeof(EmptyCartState))] Empty, [State(typeof(ActiveCartState))] Active, [State(typeof(PaidCartState))] Paid } public interface IAddableCartState { ActiveCartState Add(Product product); IEnumerable<Product> Products { get; } } public interface INotEmptyCartState { IEnumerable<Product> Products { get; } decimal Total { get; } } public abstract class AddableCartState: State<Cart>, IAddableCartState { protected AddableCartState(Cart entity): base(entity) { } public ActiveCartState Add(Product product) { Entity.Products.Add(product); Entity.StateCode = CartStateCode.Active; return (ActiveCartState)Entity.State; } public IEnumerable<Product> Products => Entity.Products; } public class EmptyCartState: AddableCartState { public EmptyCartState(Cart entity): base(entity) { } } public class ActiveCartState: AddableCartState, INotEmptyCartState { public ActiveCartState(Cart entity): base(entity) { } public PaidCartState Pay(decimal total) { Entity.Total = total; Entity.StateCode = CartStateCode.Paid; return (PaidCartState)Entity.State; } public State<Cart> Remove(Product product) { Entity.Products.Remove(product); if(!Entity.Products.Any()) { Entity.StateCode = CartStateCode.Empty; } return Entity.State; } public EmptyCartState Clear() { Entity.Products.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State; } public decimal Total => Products.Sum(x => x.Price); } public class PaidCartState: State<Cart>, INotEmptyCartState { public IEnumerable<Product> Products => Entity.Products; public decimal Total => Entity.Total; public PaidCartState(Cart entity) : base(entity) { } } }
      
      





状態は、偶然にネストされたクラスとして宣言されません。 ネストされたクラスはCart



クラスの保護されたメンバーにアクセスできます。つまり、動作を実装するためにエンティティのカプセル化を犠牲にする必要はありません。 エンティティクラスファイルにCart.cs



ないようにするために、宣言を2つのpartial



キーワードに分割してCart.cs



CartStates.cs



に分けました。



パターンマッチング



異なる状態間に共通の動作がないため、制御フローにポリモーフィズムを使用できません。 ここでは、 パターンマッチングが役立ちます。



  public ActionResult GetViewResult(State<Cart> cartState) { switch (cartState) { case Cart.ActiveCartState activeState: return View("Active", activeState); case Cart.EmptyCartState emptyState: return View("Empty", emptyState); case Cart.PaidCartState paidCartState: return View("Paid", paidCartState); default: throw new InvalidOperationException(); } }
      
      





バスケットの状態に応じて、異なるビューを使用します。 空のバスケットの場合、「バスケットは空です」というメッセージを表示します。 アクティブなバスケットには、商品のリスト、商品の数を変更してそれらの一部を削除する機能、「注文する」ボタン、合計購入金額が表示されます。



有料バスケットはアクティブバスケットと同じように見えますが、何も編集する機能はありません。 この事実は、 INotEmptyCartState



インターフェイスを強調表示することでINotEmptyCartState



できます。 したがって、Liskの置換原則の違反を取り除くだけでなく、インターフェイス分離の原則も適用しました。



おわりに



アプリケーションコードでは、インターフェイスリンクIAddableCartState



INotEmptyCartState



を使用して、バスケットに商品を追加し、バスケットに商品を表示するコードを再利用できます。 型の間に共通点がない場合にのみ、パターンマッチングがC#の制御フローに適していると思います。 それ以外の場合は、ベースリンクを使用すると便利です。 同様の手法は、エンティティの動作のコーディングだけでなく、データ構造にも適用できます。



All Articles