Domain-Driven Design: Referencing Aggregates and Using DTOs
source link: http://lorenzo-dee.blogspot.com/2013/05/domain-driven-design-referencing.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
After seeing and reviewing some existing systems, I've been thinking about how to improve the way I practice domain-driven design. In this entry, let me talk about two things that I think helps when implementing DDD:
- Reference other aggregates by ID.
- Use data transfer objects.
Reference Other Aggregates by ID
Let aggregates reference other aggregates by ID (identity), not the aggregate itself.
Vaughn Vernon has written about this.
It's quite often that I'd model aggregates and reference other entities/aggregates. But this makes the system more difficult to maintain, as an aggregate would need to know every other entity/aggregate. Take the order and product domain model as an example.
We have an order entity that has one-or-more order items. The order entity forms the aggregate root. Each order item would refer to a product, a quantity, and a total price.
Just to help paint a better picture, we would also have a repository for order (OrderRepository) and a repository for product (ProductRepository).
Usually, we would have the order item reference a product by type. Something like:
class
Order {
private
Long id;
private
List<OrderItem> items;
. . .
}
class
OrderItem {
private
Product product;
private
int
quantity;
private
Money unitPrice;
}
class
Product {
private
Long id;
}
Instead of referencing product by type, we can reference it by ID, like this:
class
Order {
private
Long id;
private
List<OrderItem> items;
public
Long id() {
return
this
.id; }
public
List<OrderItem> items() {
return
Collections.unmodifiableList(
this
.items); }
. . .
}
class
OrderItem {
private
Long productId;
private
int
quantity;
private
Money unitPrice;
. . .
}
class
Product {
private
Long id;
. . .
}
NOTE: I'm using the Long type for simplicity here. Otherwise, a domain-related value object like ProductId could be used.
NOTE: Notice how getters are not used to expose read-only properties in the Order entity. This seems to be a pattern to help developers get off the JavaBean/getter-setter mindset, and stay with a domain object/entity mindset.
This allows a cleaner implementation, since the order aggregate would not be referencing the product. This also makes the ProductRepository more useful. Otherwise, products would have been retrieved using orders.
If you happen to be using JPA for persistence, you can use the @ManyToOne annotation and specify the targetEntity element:
@Entity
class
Order {
@Id
private
Long id;
@OneToMany
private
List<OrderItem> items;
. . .
}
@Entity
class
OrderItem {
@ManyToOne
(targetEntity = Product.
class
)
private
Long productId;
private
int
quantity;
private
Money unitPrice;
. . .
}
@Entity
class
Product {
@Id
private
Long id;
. . .
}
NOTE: Notice that a uni-directional relation between order and order items is used (not a bi-directional relation). This helps make the implementation simpler, as the order item does not reference its parent order.
The above results to the following DDL.
create
table
"order"
(. . .);
create
table
order_item (
. . .
product_id
bigint
,
. . .
);
create
table
product (
id
bigint
generated
by
default
as
identity (start
with
1),
. . .
);
. . .
alter
table
order_item
add
constraint
FKxxxxxxxxxxx2
foreign
key
(order_id)
references
order
;
. . .
alter
table
order_item
add
constraint
FKxxxxxxxxxxx7
foreign
key
(product_id)
references
product;
. . .
Given the above simple adjustment, we now have an order aggregate that does not directly reference the product aggregate. This creates a cleaner separation and a more modular domain model. It might even be possible to have an order that can reference a different type of product (as long as the ID type is compatible). This might allow a more re-usable order domain model.
Use Data Transfer Objects
Use simple structures (DTOs) to transfer data from the domain model to an interface and vice-versa.
I often see the domain model entities with all of its fields being modifiable via getters and setters. Also, they have zero-arguments constructors. This makes it difficult to apply invariants or business rules (as the entity can be in an invalid state). And the usual excuse I get for this is because they need to create a UI that allows the fields/properties to be modifiable.
For me, I'd rather have an entity that is always valid.
To preserve the business rules or invariants of the domain model, we cannot afford to have all of the entity fields being modifiable. This is where DTOs are needed.
package
order.domain.model;
@Entity
class
Order {
. . .
public
int
addItem(Long productId,
int
quantity, Money unitPrice) {
. . .
}
}
@Entity
class
OrderItem {
}
. . .
package
order.interfaces.dto;
class
OrderDTO {
private
List<OrderItemDTO> items;
public
List<OrderItemDTO> getItems() {...}
public
void
setItems(List<OrderItemDTO> ...) {...}
}
class
OrderItemDTO {
private
Long productId;
private
int
quantity;
// getters and setters
}
The UI shall use the DTOs when displaying and receiving inputs. Once the data is captured in DTOs, assemblers are used to apply the changes to domain objects. This pattern can be seen in the dddsample project.
package
order.interfaces.assembler;
class
OrderAssembler {
. . .
public
OrderDTO toDTO(Order order) {
OrderDTO dto =
new
OrderDTO();
. . .
dto.setOrderDate(order.orderDate());
dto.setEntryDate(order.entryDate());
dto.setCustomerId(order.customerId());
. . .
return
dto;
}
. . .
public
Order fromDTO(OrderDTO dto) {
Order order;
if
(dto.getId() !=
null
) {
order = orderRepository.find(dto.getId());
}
else
{
order =
new
Order(..., dto.getCustomerId());
}
. . .
Long productIds[] = dto.getProductIds();
// Can use IN operator to get products with given IDs
Map<Long, Product> products = productRepository.find(productIds);
for
(OrderItemDTO itemDTO : dto.getItems()) {
Product product = products.get(itemDTO.getProductId());
order.addItem(
item.getProductId(),
item.getQuantity(),
product.getPrice());
}
return
order;
}
. . .
}
This has helped me understand DDD implementations better. I hope it helps others too.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK