Domain Modeling
(Notes on Architecture Patterns with Python by Harry Percival, Bob Gregory: Chapter 1 - Domain Modeling)
What I Liked About This Chapter
- Clear definitions of value objects and entities.
- Emphasis that not everything has to be an object: introducing the concept of a domain service function.
- Highlighting how exceptions can express domain concepts.
In my own words, domain modeling is a way to approach software design by creating abstractions rooted in business logic. When translating business requirements into code becomes challenging, we can simplify the process by adhering to domain-specific language and constructing layers of objects aligned with the domain. Concepts like value objects and entities help us categorize the objects we create, although the categorization isn't always straightforward.
Value Objects and Entities
Value Object: A domain object that has data but no identity.
- A value object is uniquely identified by the data it holds (two value objects are equal if all of their properties are equal).
- They are typically immutable.
Examples:
- Money: All 10 dollar bills are the same in terms of monetary value, regardless of their serial numbers.
- Name: Two names are considered equal if both the first and last names match.
Value objects do not always equate to "data classes". They can still have complex behavior. We usually implement equality using strict equality—if every attribute of the object is identical, the objects are considered the same.
Entity: A domain object with a long-lived identity.
- Entities have identity equality. Their values can change, yet they are still considered the same object.
Examples:
- Person: People can change their names or marital status, but they remain the same individual.
Entities typically implement equality based on a unique reference or ID rather than on the equality of all attributes.
My Experience
Designing with value objects and entities in mind has been a mixed experience for me. It’s often unclear what qualifies strictly as an entity or a value object. For example, in a project where I needed to define the concept of a "dependency" (such as a node module or a pip package), it wasn't clear if it should be treated as an entity or a value object.
Reasons It Might Be an Entity:
- The version of the dependency can change frequently, raising the question of whether different versions are still considered the same entity.
Reasons It Might Be a Value Object:
- Some dependencies differ significantly across versions (e.g., a V1 and V2 split into separate repositories, with the older one deprecated). In this case, each exact version could be viewed as a value object if its attributes remain consistent.
I was concerned that treating something as a value object (by implementing it as a data class without behavior) would prevent encapsulation of relevant logic. However, the book suggests that
"We can still have complex behavior on a value object, such as mathematical operators on money.""
This perspective helps address the above dilemma: it may not be crucial which category an object belongs to—it can be context-driven. Depending on the use case, an object may better fit as a value object or an entity.
Not Everything Has to Be an Object
Given the above definitions, I appreciated the authors' clarification that not everything needs to be an object.
"For every
FooManager
,BarBuilder
, orBazFactory
, there's often a more expressive and readablemanage_foo()
,build_bar()
, orget_baz()
function."
In essence, instead of always constructing a domain object, consider using a Domain Service Function if it better encapsulates the business logic.
Using Exceptions to Capture Domain Concepts
Lastly, edge cases and failures can be captured with exceptions (or errors), which are excellent tools for expressing domain logic. I find that defining errors provides clarity to domain design, especially since errors often need to be surfaced to the end user. This dual visibility—both internal and external—creates a solid basis for conversations with domain experts.