4 ms·
I don't get it. It is perfectly valid to use immutability and the Type system to enforce those invariants in c#. abstract class User { public stri
by thomasz 11y ago
I don't get it. It is perfectly valid to use immutability and the Type system to enforce those invariants
in c#.
abstract class User {
public string Name { get; }
abstract bool IsActive { get; }
public User(string name) {
this.Name = name;
}
}
class ActiveUser : User {
public ActiveUser(string name) : base(name) { }
private ICollection<Subscription> _subscriptions = new List<Subscription>()
public IEnumerable<Subscription> Subscriptions => _subscriptions;
IsActive => true;
public DeletedUser Delete() => new DeletedUser(Name, DateTime.Now);
public void AddSubscription(Subscription s) => Subscriptions.Add(subscription);
}
class DeletedUser : User {
public DeletedUser(string name) : base(name) { }
public DateTime DeletionDate {get;}
IsActive => false;
public DeletedUser(string name, DateTime deletionDate) : base(name) {
this.DeletionDate = deletionDate;
}
public ActiveUser Reactivate() => new ActiveUser(Name);
}
- vkhorikov 11y agoYou are right, inheritance is perfectly applicable to the example from the article, basically because the example is very simple. The idea is that in more complex scenarios the best practice is to prefer composition over inheritance because modelling every single piece of state with subclasses would be sloppy and unmaintainable. That is where design-by-contract (or simply put guard clauses) usually come into play.
- kvb 11y agoBut someone else could create a subclass breaking the invariants. It's possible to avoid this by making the abstract class's constructor private and nesting the two subclasses inside of the abstract class, but this is not idiomatic. If you have to work that hard then most people won't bother, while in F# it's very natural to create such a design.
- thomasz 11y agoI do not understand how you want to break the invariants by subclassing. My ActiveUser can be deleted and my DeletedUser can be reactivated. How do you break that?
- seanmcdirmid 11y agoIt is easy to do in C# and I do it often (seal your concrete subclasses if you are paranoid). F# does provide better syntax for it though...I wouldn't be surprised if under the covers they are using classes. C# still doesn't provide pattern matching to make it very useful...but someday I hope that happens.
- saosebastiao 11y agoYes, it does use classes under the covers. I've never consistently used an OO language before, but I've been using C# lately, and knowing how F# can be built on top of a VM that was built for C# has definitely helped me to bring some of these concepts with me. I'm probably not writing idiomatic C#, but I think I'm okay with that.
- achr2 11y agoI think all of this hinges on your situation, but personally I would not want to do this. I would prefer to have check constraints on every method than have to bolt immutability into most C# paradigms (UI, Web) where it is not a first class citizen.
- steego 11y agoYou have a point, and I think the source of the author's confusion is the article never actually demonstrates the need for union types. Simply put, his example could have been simplified to this based on his use cases: type ActiveUser = { Name: string; Subscriptions: Subscription list } type DeletedUser = { Name: string; DeletedDate: DateTime } let delete (activeUser:ActiveUser) = { Name = activeUser.Name; DeletedDate = DateTime.Now } let undelete (deletedUser:DeletedUser) = { Name = deletedUser.Name; Subscriptions = [] } let addSubscription subscription activeUser = { activeUser with Subscriptions = subscription::activeUser.Subscriptions} Your approach with subtyping is a valid approach, but you should note that your example doesn't actually create immutable objects due to the way you add subscriptions. I converted your example to F#, so you can appreciate how subtyping in F# has a very natural flow that makes writing types very similar to writing functions. type User internal(name:string, isActive:bool) = member this.Name = name member this.IsActive = isActive type ActiveUser private(name:string, subscriptions:Subscription list) = inherit User(name, true) member this.Subscriptions = subscriptions :> IEnumerable<_> member this.AddSubscription(s) = ActiveUser(name, s::subscriptions) new(name) = ActiveUser(name, []) type DeletedUser(name, deletedDate:DateTime) = inherit User(name, false) member this.DeletedDate = deletedDate member this.Reactivate() = ActiveUser(name)