Intention Revealing Designs
Join the DZone community and get the full member experience.
Join For FreeWhat do you think about, really think about, when you sit down with a blank paper (or code editor) to solve a problem? Do you ever think about the next person picking up that page (or piece of code) and trying to understand your solution? Do you ever reflect back on your scribbled pages or code and wonder if your solution actually reveals a clear intention of a solution to the problem? These questions are not trivial. There are philosophical whirlpools spinning around in them. But let's just wade in the shallows for now. Baby steps.
One of the key characteristics of a good design and good code is that it is unambiguous and explicitly clear in its intention. When the intentions of designs and code are made clear, then readability and communication increases and high maintenance costs are reduced. Unless we are consciously aware of the fact that we design for others, it's easy to fall into the trap of focusing on implementations and not intentions. There are several symptoms that you can keep a beady eye on to avoid the trap. Here are two symptoms that I often come upon.
Symptom 1: Implementation Thinking
Often the problem lies in carrying implementation details into the naming of methods meant to describe the intention. For example, assume you have designed a social network as a directed graph and you want to find all circles of friends that are in this social network (e.g. Joe knows Mary who knows Peter who knows Joe). Since it's a graph, we can find all cycles in the graph, or closed loops. So, you write code such as:
class ObjectGraph {
public Collection<CyclicGraph> findCyclicSubgraph() {
...
}
}
class CyclicGraph extends ObjectGraph {
...
}
This does not reveal any intention of finding circles of friends nor does it surface the intention of the domain adequately. The same code with intention revealing names just reads better:
class SocialNetwork {
public Collection<SocialNetwork> findCirclesOfFriends() {
...
}
}
Lesson to be learnt: Really think about naming. Don't forget that object orientation is about conversations of messages. If your messages are poor, the conversation is poor and the code is just bad prose.
Symptom 2: Side Effects
Read the following code and consider what you actually think while reading it.
ShoppingCart = customer.getShoppingCart();
Money sumPricesExcludingTax = shoppingCart.addItem(selectedBook);
if (customer.getCreditLimit().lessThan(sumPrices)) {
throw new CreditLimitExceededException(customer);
}
List products = shoppingCart.getItems();
// ... display updated shopping cart items and totals
My thoughts were "What else is happening inside the shoppingCart object because it requires me to remember the new total each time an item is added? The customer object is oblivious of the purchases, so do I need to do similar credit limit tests when shopping cart items are updated, removed, etc?"
The first problem is that shoppingCart.addItem() introduces a side effect that must be tracked based on the total value of the products that does not include tax. Secondly, the responsibility is split across objects causing the side effect CreditLimitExceededException to be thrown. Thirdly, ShoppingCart is a metaphorical implementation of the the products being purchased and is actually an implementation detail of Customer.
Here's the improvement:
try {
customer.buy(selectedBook);
} catch (CreditLimitExceededException e) {
// handle the exception
} finally {
Collection products = customer.productsInShoppingCart();
Money totalExcludingTax = customer.calculateTotalExcludingTaxes();
// display the list of items in the cart and total etc.
}
In this code, the responsibility has been delegated to the Customer, the CreditLimitExceededException side effect of buying another product is caught and dealt with explicitly (and not propagated onwards) and the metaphor of a shopping cart is not lost but it's implementation is hidden.
Here is another but more subtle example and, again, consider what you actually think while reading it.
if (!user.isAccountActive()) {
user.setAccountActive(true);
}
My thoughts were "What would happen if user.setAccountActive() was called twice? Is there something that I am not aware of?" Even though there is no side-effect in user.setAccountActive(), the surrounding code creates the illusion that something else may be going on that is not clear.
Here's the simple fix that defers the detail to an implementation hidden in the method.
user.activateAccount();
Lesson to be learnt: Aim for messages that have zero side effects. Along with intention revealing names and well formed messages, aim for conversations that tell a story, rather than ask questions.
Back to Philosophical Reflections
Whenever you are writing code, ask yourself: "Is my intention clear and unambiguous?" If the answer is "No!" then your code is most probably lacking readability. True, there are refactoring techniques and many patterns that can be used to increase the suppleness of your design, but just spending a few extra brain cycles focusing on what others will think when using your work will increase readability and reduce maintenance. Seriously, you don't want every line of code to follow you around, so write for others and not just for yourself.
Published at DZone with permission of Aslam Khan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments