InvalidOperationException
.For more details on the example of F #, the topic make illegal states unrepresentable is disclosed on the Scott Vlashin website .
[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; } }
EmptyCartState
, ActiveCartState
and PaidCartState
cannot implement one interface. They can, but such an interface should contain only the methods available in each state. In our case, the Add
method is available in EmptyCartState
and ActiveCartState
, so you can inherit them from the abstract AddableCartStateBase
. However, you can add goods only to the unpaid shopping cart, so there will not be a common interface for all states. Thus, we guarantee the absence of InvalidOperationException
in our code at the compilation stage. 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
class, which means we don’t have to sacrifice the encapsulation of the entity to implement the behavior. In order not to litter the entity class file, I divided the declaration into two: Cart.cs
and CartStates.cs
using the partial
keyword. 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
interface. Thus, we not only got rid of the violation of the Liskov substitution principle, but also applied the principle of interface separation.IAddableCartState
and INotEmptyCartState
interface links to reuse the code responsible for adding products to the cart and displaying the items in the cart. I believe that pattern matching is suitable for control flow in C # only when there is nothing in common between types. In other cases, the work on the base link is more convenient. A similar technique can be applied not only to encode the behavior of an entity, but also to the data structure .Source: https://habr.com/ru/post/341134/
All Articles