Side Effecting Functions Are Code Smells Revisited
Intention Revealing Interfaces
Let's step back a little bit form the discussion we had earlier. Let's talk about good design for a second. How many times have you seen a method and had no idea what it did or that it went ahead and called 15 other things that you didn't expect? At that point, most people would take out .NET Reflector (a Godsend BTW) and dig through the code to see the internals. One of the examples of the violators was the ASP.NET Page lifecycle when I first started learning it. Init versus Load versus PreLoad wasn't really exact about what happened where, and most people have learned to hate it.
In the Domain Driven Design world, we have the Intention Revealing Interface. What this means is that we need to name our classes, methods, properties, events, etc to describe their effect and purpose. And as well, we should use the ubiquitous language of the domain to name them appropriately. This allows other team members to be able to infer what that method is doing without having to dig in with such tools as Reflector to see what it is actually doing. In our public interfaces, abstract classes and so on, we need to specify the rules and the relationships. To me, this comes back again to DbC. This allows us to not only specify the name in the ubiquitous language, but the behaviors as well.
Command-Query Separation (CQS)
Dr. Bertrand Meyer, the man behind Eiffel and the author of Object-oriented Software Construction, introduced a concept called Command-Query Separation. It states that we should break our functionality into two categories:
- Commands - Methods that perform an action or change the state of the system should not return a value.
- Queries - Return a result and do not change the state of the system (aka side effect free)
Of course this isn't a 100% rule, but it's still a good one to follow. Let's look at a simple code example of a good command. This is simplified of course. But what we're doing is side effecting the number of items in the cart.
public class ShoppingCart
{
public void AddItemToCart(Item item)
{
// Add item to cart
}
}
Should we use Spec# to do this, we could also check our invariants as well, but also to ensure that the number of items in our cart has increased by 1.
public class ShoppingCart
{
public void AddItemToCart(Item item)
ensures ItemsInCart == old(ItemsInCart) + 1;
{
// Add item to cart
}
}
So, once again, it's very intention revealing at this point that I'm going to side effect the system and add more items to the cart. Like I said before, it's a simplified example, but it's a very powerful concept. And then we could talk about queries. Let's have a simple method on a cost calculation service that takes in a customer and the item and calculates.
public class CostCalculatorService
{
public double CalculateCost(Customer c, Item i)
{
double cost = 0.0d;
// Calculate cost
return cost;
}
}
What I'm not going to be doing in this example is modifying the customer, nor the item. Therefore, if I'm using Spec#, then I could mark this method as being [Pure]. And that's a good thing.
The one thing that I would hold an exception for is fluent builders. Martin Fowler lays out an excellent case for them here. Not only would we be side effecting the system, but we're also returning a value (the builder itself). So, the rule is not a hard and fast one, but always good to observe. Let's take a look at a builder which violates this rule.
public class CustomerBuilder
{
private string firstName;
public static CustomerBuilder New { get { return new CustomerBuilder(); } }
public CustomerBuilder WithFirstName(string firstName)
{
this.firstName = firstName;
return this;
}
// More code goes here
}
To wrap things up, things are not always fast rules and always come with the "It Depends", but the usual rule is that you can't go wrong with CQS.
Wrapping It Up
These rules are quite simple for revealing the true intent of your application while using the domain's ubiquitous language. As with anything in our field, it always comes with a big fat "It Depends", but applying the rules as much as you can is definitely to your advantage. These are simple, yet often overlooked scenarios when we design our applications, yet are the fundamentals.