Why do you struggle with this in C#, especially given that you are familiar with the F# style? I don't know C#, but in Java I'd write this as:
public class Order {
final Optional<DeliveryDetails> delivery;
public Order(Optional<DeliveryDetails> delivery) {
this.delivery = delivery;
}
}
public class DeliveryDetails {
final long deliveryTimeMs;
...
public DeliveryDetails (long deliveryTimeMs,...) {
this.deliveryTimeMs = deliveryTimeMs;
...
}
}
My IDE writes most of these lines for me. I believe C# will have similar or more succinct constrcuts.
The ide maybe will write a small part of it, but you’ll have to keep reading it forever.
And obviously it is modelled wrong because it is possible to have a Delivered order without delivery details, there is nothing that enforces it.
Compare it with how I would write it in f#:
type OrderId = OrderId of string
type DeliveryTime = DeliveryTime of long
type DeliveryDetails = { deliveryTime: DeliveryTime...}
type Order = { id: OrderId ...}
type DeliveredOrder = { id: OrderId, deliveryDetails: DeliveryDetails...}
Probably it is even better to define delivery time using the unit of measures and specifying it as ms.
What is the difference in this way?
That an order cannot ever have DeliveryDetails, while a DeliveredOrder must have DeliveryDetails.
As a bonus you can’t just pass any string as an order id (for example a description) but you need to pass an actual order id. the same is true for the deliveryTime with the adddd advantage that you won’t be able to perform operations on it with a different unit of measure.
In java or c# you would kill yourself if you try to do something similar and moreover you won’t have all the constraints specified here and the immutability automatically enforced.
they said "an order cannot ever have DeliveryDetails"
so, if in a nominative subtyping situation like you suggest, a DeliveredOrder is-a Order, and so we can see that some subset of Orders CAN have DeliveryDetails. any method receiving an Order could receive a DeliveredOrder.
No, most of the time is wrong.
If you have a DeliveredOrder you want to make sure that is not delivered again, so the delivery function should accept only an Order, not a DeliveredOrder.
If in some different system you need a domain object that is both an Order and a DeliveredOrder than in F# you simply use an union type:
type Order = UndeliveredOrder of UndeliveredOrder | DeliveredOrder of DeliveredOrder
And in this way you can write a function that accepts both an UndeliveredOrder and a DeliveredOrder.
perhaps, perhaps not. in the abstract there's no way to valuate it. it depends on what it means to be an Order and what it means to be a DeliveredOrder, what assumptions are made by code that receives an Order, etc.
> i agree it will take much more time to implement that in c#.
If I am reading the code correctly, here is the Java version (with Lombok [1]):
@Data class OrderId { final String id; }
@Data class DeliveryTime { final long time; }
@Data class DeliveryDetails { final DeliveryTime deliveryTime; }
@Data class Order { final OrderId id; }
@Data class DeliveredOrder {
final OrderId id;
final DeliveryDetails deliveryDetails;
}
I can imagine C# also having similar expressive powers.
[1] http://jnb.ociweb.com/jnb/jnbJan2010.html. Lombok reduces the drudgery of writing some of the code in Java. Modern JVM languages like Scala and Kotlin have native constructs to express this.
If you are not writing java and you are using lombok then yes, this simple case seems covered well.
I still can’t see how Lombok will help with the exhaustive pattern matching in case the order is a union type of several orders types as explained in my other comment or with avoiding mixing up ms and seconds when using a unit of measure in f# for deliveryTime.
Also I’m curious how you would change just one field of an immutable object with 10s of properties in Lombok and if the resulting java code is as efficient as F# with its immutable data structures that use the copy on write semantics.
I agree that exhaustiveness check enforced by compiler is something I will miss in Java.
Persistent data structures have been implemented in Java if that's what you mean by efficient mutations to immutable structures. I can't imagine such structures being hard in any language hosted on the JVM or CLR.
F#, and ML-family languages, surely have their killer features. I am only contesting the claim that the GP made that modeling a domain is a struggle in C# when compared to F#.
Imagine if someone came on this thread and claimed that they struggle to write effectful code in F# which they have been writing in Haskell. Of course, you have counter-evidence of that in all the F# programs you have written so far! I feel the same about the inability-to-domain-modeling claim.
that's true, you can recreate it in some sense but you can't get to what F# can guarantee. A big promise of discriminated unions is the ability to make invalid state unrepresentable. Matching on the different type constructors is a fantastic way to only express coherent states.
Terminology nitpick: Pattern matching is done on value constructors (or just "constructors", but at the intersection of FP and OOP that could be confusing).
"type constructor" means something like `List` (as opposed to `List[Int]`) – a generic type that hasn't been applied to any type argument(s) yet, and will "construct" a type (like `List[int]`) when you apply it.