Well, it was just the beginning ...
I know we're a bit late, but let's go now with the second part of my How to avoid getters and setters series ... I hope you'll enjoy it!
Avoid your getters and setters : The Expert, the Aggregate and the Value Object.
Many people use getters and setters so extensively because don't really get the importance of correctly assigning object responsibilities and behaviors.
So, in this second part we'll mix together GRASP and Domain Driven Design patterns, in particular:
We'll see how wrongly managing responsibilities and behaviors, while abusing getters and setters, can lead to a corrupted business domain, and how the patterns above will help you to correctly address these kind of problems.
The business domain.
Let's start our discussion by showing a simple business domain.
We're implementing yet another ugly e-commerce system, so we have a shopping cart holding a number of orders, each referring to a given product.
Without going further on, let's restrict our domain model to three simple entities: ShoppingCart, Order and Product.
A ShoppingCart can hold one or more Orders, and an Order refers just to a single Product.
Moreover, we have some simple business rules regarding these entities:
- The ShoppingCart total is the sum of all Order sub-totals.
- Each Order sub-total is the Product price times the ordered Product quantity.
- The ShoppingCart total cannot exceed the value of EUR 10000.
That's all.
Very simple, isn't it?
Let's implement it : the wrong way.
The entities and rules above can be straightforwardly implemented as follows:
public class Product {
private String name;
// ...
private double price;
// ...
public double getPrice() {
return this.price;
}
public void setPrice(double price) {
this.price = price;
}
}
public class Order {
private Product product;
private int quantity;
public Product getProduct() {
return this.product;
}
public void setProduct(Product p) {
this.product = p;
}
public int getQuantity() {
return this.quantity;
}
public void setQuantity(int q) {
this.quantity = q;
}
}
public class ShoppingCart {
private List orders = new ArrayList();
public void addOrder(Order o) {
this.orders.add(o);
}
public List getOrders() {
return this.orders;
}
}
As you can see, all classes have the right properties and dependencies:- A Product has a price.
- An Order refers a Product and has a product quantity.
- The ShoppingCart has more orders.
- The ShoppingCart total can be calculated by iterating all Orders and summing up each Product price times the ordered product quantity.
- There's no clear assignment of responsibilities and behaviors.
- Hence, it is full of getters and setters.
- Hence, there's no encapsulation.
- Hence, all business logic is external.
- Hence, it is easy to corrupt domain state and violate business rules.
Given our design and implementation, the following code is absolutely legal:
Product p1 = new Product();
Product p2 = new Product();
p1.setPrice(1000);
p2.setPrice(2000);
Order o1 = new Order();
Order o2 = new Order();
o1.setProduct(p1);
o1.setQuantity(5);
o2.setProduct(p2);
o2.setQuantity(5);
ShoppingCart cart = new ShoppingCart();
cart.addOrder(o1);
cart.addOrder(o2);
But hey, wait!The ShoppingCart total is now EUR 15000!
That's because the business logic is computed outside of the object that holds the information; so let's solve this problem by introducing the Information Expert pattern.
Refactoring toward the Expert.
The Information Expert pattern is part of the General Responsibility Assignment Software Patterns (GRASP), and states the following:
Assign a responsibility to the information expert: the class that has the information necessary to fulfill the responsibility.We have two responsibilities to relocate:
- The shopping cart total computation: here, the information expert is the ShoppingCart class.
- The order sub-total computation: here, the information expert is the Order class.
public class Order {
private Product product;
private int quantity;
public void setProductWithQuantity(Product p, int q) {
this.product = p;
this.quantity = q;
}
public double computeSubTotal() {
return this.product.getPrice() * this.quantity;
}
}
public class ShoppingCart {
private List orders = new ArrayList();
public void addOrder(Order o) {
this.orders.add(o);
}
public double computeTotal() {
double total = 0;
Iterator it = this.orders.iterator();
while (it.hasNext()) {
Order current = (Order) it.next();
total += current.computeSubTotal();
if (total > MAX_TOTAL) {
throw new SomeException();
}
}
return total;
}
}
As you can see, the ShoppingCart computeTotal() method is now able to check against the maximum total, and throw an exception if there's something wrong, avoiding domain state corruption.However, there's still something wrong; take a look at the following code:
Product p1 = new Product();
Product p2 = new Product();
p1.setPrice(1000);
p2.setPrice(2000);
Order o1 = new Order();
Order o2 = new Order();
o1.setProductWithQuantity(p1, 2);
o2.setProductWithQuantity(p2, 4);
ShoppingCart cart = new ShoppingCart();
cart.addOrder(o1);
cart.addOrder(o2);
cart.computeTotal();
// !!!!!!!!! DANGER !!!!!!!!!!
o1.setProductWithQuantity(p1, 3);
// !!!!!!!!! DANGER !!!!!!!!!!
The problem is that we can always change the Order product and quantity, changing so the shopping cart total without preventing it to enter in an invalid state!Where's the real problem?
How to solve it?
Refactoring toward the Aggregate and the Value Object.
The real problem is that we are interested in keeping the ShoppingCart state always correct, that is, in keeping its invariants: however in the current design and implementation we are not able to control everything happens inside the ShoppingCart, because we can directly modify its Orders without going through it!
The solution is to apply the Aggregate and Value Object patterns, part of the Domain Driven Design.
An Aggregate is a set of related objects whose invariants must always be kept consistent, and an Aggregate Root is an object that acts like the main access point into the aggregate; all access must go through the root, and objects external to the aggregate cannot keep references to objects contained into the aggregate: they can keep a reference only to the aggregate root.
A Value Object is an object that has no identity and is immutable: it is equal to another value object of the same type if its properties are equal too, and must be discarded if its properties need to change.
How to turn this theory into practice, applying it to our domain?
First, our ShoppingCart and Order are part of an aggregate.
It's easy: the "EUR 10000" invariant involves both objects, so it must be kept consistent across the two.
Moreover, Orders make sense only if related to a ShoppingCart, so no one should access an Order without first going through a ShoppingCart: this means that the ShoppingCart is the root of the aggregate.
Second, the Order is a Value Object: it doesn't make sense to create an order and change its related product and quantity during its life cycle; an order always refers to the same product with the same quantity, and if something needs to be changed, it must be discarded and a new one must be created.
Now let's refactor our previous code:
public class Order {
private Product product;
private int quantity;
private double subTotal;
public Order(Product p, int q) {
this.product = p;
this.quantity = q;
this.subTotal = this.product.getPrice() * this.quantity;
}
public double computeSubTotal() {
return this.subTotal;
}
}
public class ShoppingCart {
private List orders = new ArrayList();
public void addOrder(Order o) {
this.orders.add(o);
}
public boolean removeOrder(Order o) {
return this.orders.remove(o);
}
public double computeTotal() {
double total = 0;
Iterator it = this.orders.iterator();
while (it.hasNext()) {
Order current = (Order) it.next();
total += current.computeSubTotal();
}
return total;
}
}
The Order class is now immutable and part of an aggregate together with the ShoppingCart.
The ShoppingCart is now the aggregate root, and every change to its Orders must go through it.
Now, you have no way of corrupting your domain.
Remember: the Expert, the Aggregate and the Value Object.
Who do you want to be?
5 comments:
Hi,
why it doesn't make sense to change the quantity of an Order instance during its life cycle?
Maybe I didn't explain it well, or maybe I've used the wrong words.
It's not that you can't change the quantity.
An order represents an ordered product and its quantity at a given time.
If you want to change the product quantity, you are allowed to do so, but you don't actually change your order: you actually discard the current one, and create a new order with the new quantity.
The difference may seem very subtle, but it is indeed very important to keep your domain state always valid.
Good stuff. One question, though. Don't you need to throw an exception or something if a client of ShoppingCart tries to add an order the would push the cart over it's currency limit?
John,
once said that this is just an example, my answer is: it depends.
You may want to let clients add things into the cart beyond its limits, and then, when the total is actually computed (say when the cart is submitted), throw an exception: by doing so, users can choose what to remove from the cart, and what to keep.
Or you may want to never go beyond limits, and throw an exception as soon as orders are added that push the cart over its limits, as you said.
The most important things are that:
1. You must not break constraints by the means of wrong coding practices.
2. Once submitted, the cart limit must not be exceeded.
Cheers,
Sergio B.
We can still change the order though by creating it outside and adding to the shopping cart.
Post a Comment