Clean Code: an example …
Sunday, September 27th, 2009A friend asked me to review some of the code in his codebase and discuss what if anything I would change to make the code cleaner.
For me the term “Clean” code means all those things outlined in the book “Clean Code” by Robert C. Martin, especially principles like the Single Responsibility Principle, the East / Tell Don’t Ask Principle and the practice of keeping things DRY (Don’t Repeat Yourself) and others. One thing I think is important but not made that clear in various texts is the benefit of keeping the “what” of Business intent clear and uncluttered from the “how” of the implementation. What follows is an example and how it was refactored to be more clean.
My friend has a class that looks through a database of Users to notify them when their Book subscriptions are about to expire. Not all the Users want to be notified when this happens. The typical implementation that I saw was a single class that gets the users, iterates through them and dispatches an email to each User that wants to be notified.
A1 public void notifyExpiredUsers() {
A2 Date date = new Date();
A3 List<User> users = getUsers();
A4 for (User user : users) {
A5 if (date > user.getExpiryDate() && user.getEmailNotification()) {
A6 sendEmailTo(user.getName(), user.getEmailAddress());
A7 }
A8 }
A9 }
We then discussed what I would like to change and why …
The first thing I would like to do is clean this method up to express the intent more clearly, with each method having a single responsibility.
B1 public void notifyUsersWhoseBookSubscriptionsHaveExpired() {
B2 List<User> bookSubscribers = getBookSubscribers();
B3 for (User bookSubscriber : bookSubscribers) {
B4 notifyUserWhoseBookSubscriptionHasExpired(bookSubscriber)
B5 }
B6 }
C1 public void notifyUserWhoseBookSubscriptionHasExpired(User bookSubscriber) {
C5 if (hasBookSubscriptionExpired(bookSubscriber)) {
C6 sendSubscriptionExpiryNotificationTo(bookSubscriberNotificationDetails(bookSubscriber));
C7 }
C9 }
* note that the method in #C6 bookSubscriberNotificationDetails() returns a new object BookSubscriberNotificationDetails.
D1 public boolean hasBookSubscriptionExpired(User bookSubscriber) {
D2 Date today = new Date();
D2 return (date > bookSubscriber.getSubscriptionExpiryDate() && bookSubscriber.getEmailNotification());
D3 }
To me the code is looking a lot better already as we have:
1. Used a name that represents the business WHAT, a “bookSubscriber” rather than the HOW, a “User”.
2. Removed several responsibilities of the first “notifyExpiredUsers()” method into separate methods with single responsibilities.
3. Replaced a combination of conditions in the if() statement (A5) with a call to a method that describes the Business intent (C5) rather than the mechanism used to find out how.
4. Introduced a new class BookSubscriberNotificationDetails in C6 to capture the binding between the notification process and where in the model the information comes from.
5. Replaced the call to send an Email in C6 with a call to a method that describes the intent rather than the how.
There is still a problem with C5 which it has carried on from A5 and that is the combination of conditions that make the code less clear. The combination of a book subscription expiring and the subscriber wanting to be notified should be exposed through the naming of the method. So lets change that.
E1 public void notifyUserWhoseBookSubscriptionHasExpired(User bookSubscriber) {
E5 if (shouldNotifyThatBookSubscriptionHasExpired(bookSubscriber)) {
E6 sendSubscriptionExpiryNotificationTo(bookSubscriberNotificationDetails(bookSubscriber));
E7 }
E9 }
F1 public boolean shouldNotifyThatBookSubscriptionHasExpired(User bookSubscriber) {
F2 return (hasBookSubscriptionExpired(bookSubscriber) && bookSubscriberWantsNotification(bookSubscriber));
F3 }
F4
F5 public boolean bookSubscriberWantsNotification(User bookSubscriber) {
F6 return bookSubscriber.getEmailNotification();
F7 }
While this is an improvement because the business intent of the code is more clear, there are still some changes we can make to the code to further to minimize change impact by hiding data and moving behavior close to the owner of the associated data. This is in line with East Principles. The first change to go East will be with D2 where the bookSubscribers subscription expiry date is exposed to the caller (travels West). Also exposed is the email notification value, so lets change them.
G1 public boolean hasBookSubscriptionExpired(User bookSubscriber) {
G2 Date today = new Date();
G3 return bookSubscriber.isSubscriptionExpiryDateBefore(today);
G4 }
G5
G6 public boolean bookSubscriberWantsNotification(User bookSubscriber) {
G7 return bookSubscriber.isEmailNotificationRequested();
G8 }
The code in G3 and G7 changed slightly to not expose values from the bookSubscriber and the remifications of this are subtle but far reaching. They are:
1. We have captured in one place the Business fact that we care about subscription expiry dates that are before other dates. At this stage we don’t have an isSubscriptionExpiryDateAfter(date), so we can tell the Business does not yet do things after an expiry. Looking through the codebase for usages of “<” or “>” would much harder.
2. We have turned a simple get of a boolean value into a descriptive Business reason for its existance.
A lot of the above modification may just appear superficial with splitting methods up or using different names and to some degree that is true but what should be more clear is the Business WHAT and not the implementation HOW. Keeping the Business WHAT clear and uncluttered from the implementation HOW is part of what Clean code is to me. We should be able to modify the implementation HOW quite significantly without effecting the Business WHAT.
Personally I would go further and make a BookSubscribers class that represents the behviour that is related to a collection of BookSubscribers. I would do it because the Business talks about Book Subscribers so this concept should make it into the domain model, and I would do it because the operations on groups of Subscribers belongs in one place and not spread around between helper classes or the methods of other unrelated classes. However, this is another post waiting to happen soon.
So is the result cleaner and more clear, please let me know what you think.
