In chapter 5, p.95, Scott writes:
"First, is Order an Entity or a Value Object? Obviously it’s an Entity—the details of the order may change over time, but it’s the same order.
What about an OrderLine, though? If we change the quantity of a particular order line, for example, is it still the same order line? In most designs, it would make sense to say yes, it is still the same order line, even though the quantity or price has changed over time. So OrderLine is an Entity too, with its own identifier.
But now here’s a question: if you change an order line, have you also changed the order that it belongs to?
In this case, it’s clear that the answer is yes: changing a line also changes the entire order. In fact, having immutable data structures makes this unavoidable. If I have an immutable Order containing immutable OrderLines, then just making a copy of one of the order lines does not also make a copy of the Order as well. In order to make a change to an OrderLine contained in an Order, I need to make the change at the level of the Order, not at the level of the OrderLine.
For example, here’s some pseudocode for updating the price of an order line:
/// We pass in three parameters:
/// * the top-level order
/// * the id of the order line we want to change
/// * the new price
let changeOrderLinePrice order orderLineId newPrice =
// 1. find the line to change using the orderLineId
let orderLine = order.OrderLines |> findOrderLine orderLineId // 2. make a new version of the OrderLine with the new price
let newOrderLine = {orderLine with Price = newPrice}
// 3. create a new list of lines, replacing // the old line with the new line
let newOrderLines =
order.OrderLines |> replaceOrderLine orderLineId newOrderLine
// 4. make a new version of the entire order, replacing // all the old lines with the new lines
let newOrder = {order with OrderLines = newOrderLines}
// 5. return the new order
newOrderThe final result, the output of the function, is a new Order containing a new list of lines, where one of the lines has a new price. You can see that immutability causes a ripple effect in a data structure, whereby changing one low-level component can force changes to higher-level components too."
where Order and OrderLine are defined in the book like this:
type Order = {
Id : OrderId // id for entity
CustomerId : CustomerId // customer reference
ShippingAddress : ShippingAddress
BillingAddress : BillingAddress
OrderLines : OrderLine list
AmountToBill : BillingAmount
}
and OrderLine = {
Id : OrderLineId // id for entity
OrderId : OrderId
ProductCode : ProductCode
OrderQuantity : OrderQuantity
Price : Price
}I'm not fully convinced of having the Order aggregate having a changeOrderLinePrice function that takes an orderLineId as one of its parameters... isn't this forcing the clients using this function to know what the id of the order line is in order to change the price?
Also, are we sure that OrderLine should be modelled as an Entity? Can we consider it instead a ValueObject composed by a ProductCode, OrderId, Quantity and Price?
DeepSeek response
Your concerns about the design choices in the book "Domain Modeling Made Functional" are valid and worth exploring. Let's break down the issues you raised:
1. OrderLine as an Entity vs. Value Object
Entity
An Entity is defined by its identity, which remains consistent over time, even if its attributes change. In the context of an
OrderLine, if you consider it an Entity, it implies that theOrderLinehas a unique identity (OrderLineId) and can change its attributes (likeQuantityorPrice) while still being the sameOrderLine.Value Object
A Value Object, on the other hand, is defined by its attributes and does not have a conceptual identity. If two Value Objects have the same attributes, they are considered equal. If you model
OrderLineas a Value Object, it would mean that any change to its attributes (likeQuantityorPrice) would result in a completely newOrderLine.Which one to choose?
Entity: If you need to track changes to an
OrderLineover time or if theOrderLinehas a lifecycle that is important to your domain, then modeling it as an Entity makes sense. For example, if you need to maintain a history of changes to anOrderLineor if theOrderLineis referenced by other parts of the system, it should be an Entity.Value Object: If the
OrderLineis simply a collection of attributes that describe a product in an order and you don't need to track changes to it individually, then modeling it as a Value Object might be more appropriate. In this case, changing any attribute of theOrderLinewould result in a newOrderLineinstance.2.
changeOrderLinePriceFunction andOrderLineIdThe function
changeOrderLinePricetakes anorderLineIdas a parameter, which implies that the client needs to know theOrderLineIdto change the price. This design choice has implications:Coupling: This design couples the client to the internal structure of the
Orderaggregate. The client needs to know theOrderLineIdto make changes, which might not be ideal if the client should only interact with theOrderaggregate without knowing its internal details.Alternative Design: Instead of exposing the
OrderLineIdto the client, you could design theOrderaggregate to handle changes internally. For example, you could have a method on theOrderaggregate that takes aProductCodeand anewPrice, and theOrderaggregate would find the correspondingOrderLineand update its price. This way, the client doesn't need to know about theOrderLineId.3. Immutability and Ripple Effects
The book emphasizes immutability, which is a common practice in functional programming. When you change an
OrderLine, you create a newOrderLineinstance, and consequently, a newOrderinstance with the updatedOrderLine. This ripple effect is inherent in immutable data structures and ensures that the entire aggregate remains consistent.4. Revised Design Considerations
Given your concerns, here’s how you might reconsider the design:
Option 1:
OrderLineas a Value ObjectIf you model
OrderLineas a Value Object, you would not need anOrderLineId. Instead, theOrderLinewould be identified by its attributes (ProductCode,OrderId,Quantity,Price). Changing any attribute would result in a newOrderLine.In this case, the
changeOrderLinePricefunction would not need anOrderLineId:Option 2:
OrderLineas an Entity with EncapsulationIf you decide to keep
OrderLineas an Entity, you could encapsulate the logic within theOrderaggregate to avoid exposingOrderLineIdto the client:In this design, the client only needs to know the
ProductCodeand the new price, not theOrderLineId.Conclusion
The choice between modeling
OrderLineas an Entity or a Value Object depends on the specific requirements of your domain. If you need to track changes to individualOrderLinesor if they have a lifecycle, then modeling them as Entities makes sense. However, ifOrderLinesare simply descriptive and do not need to be tracked individually, modeling them as Value Objects might be more appropriate.Additionally, consider encapsulating the logic within the
Orderaggregate to avoid exposing internal details likeOrderLineIdto the client, which can lead to a more maintainable and decoupled design.