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
newOrder
The 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 theOrderLine
has a unique identity (OrderLineId
) and can change its attributes (likeQuantity
orPrice
) 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
OrderLine
as a Value Object, it would mean that any change to its attributes (likeQuantity
orPrice
) would result in a completely newOrderLine
.Which one to choose?
Entity: If you need to track changes to an
OrderLine
over time or if theOrderLine
has 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 anOrderLine
or if theOrderLine
is referenced by other parts of the system, it should be an Entity.Value Object: If the
OrderLine
is 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 theOrderLine
would result in a newOrderLine
instance.2.
changeOrderLinePrice
Function andOrderLineId
The function
changeOrderLinePrice
takes anorderLineId
as a parameter, which implies that the client needs to know theOrderLineId
to change the price. This design choice has implications:Coupling: This design couples the client to the internal structure of the
Order
aggregate. The client needs to know theOrderLineId
to make changes, which might not be ideal if the client should only interact with theOrder
aggregate without knowing its internal details.Alternative Design: Instead of exposing the
OrderLineId
to the client, you could design theOrder
aggregate to handle changes internally. For example, you could have a method on theOrder
aggregate that takes aProductCode
and anewPrice
, and theOrder
aggregate would find the correspondingOrderLine
and 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 newOrderLine
instance, and consequently, a newOrder
instance 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:
OrderLine
as a Value ObjectIf you model
OrderLine
as a Value Object, you would not need anOrderLineId
. Instead, theOrderLine
would be identified by its attributes (ProductCode
,OrderId
,Quantity
,Price
). Changing any attribute would result in a newOrderLine
.In this case, the
changeOrderLinePrice
function would not need anOrderLineId
:Option 2:
OrderLine
as an Entity with EncapsulationIf you decide to keep
OrderLine
as an Entity, you could encapsulate the logic within theOrder
aggregate to avoid exposingOrderLineId
to the client:In this design, the client only needs to know the
ProductCode
and the new price, not theOrderLineId
.Conclusion
The choice between modeling
OrderLine
as an Entity or a Value Object depends on the specific requirements of your domain. If you need to track changes to individualOrderLines
or if they have a lifecycle, then modeling them as Entities makes sense. However, ifOrderLines
are 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
Order
aggregate to avoid exposing internal details likeOrderLineId
to the client, which can lead to a more maintainable and decoupled design.