The Importance of Mutually Exclusive/Collectively Exhaustive (MECE)
Liskov Substitutability is one of the elements of SOLID), a popular phrase these days referring to a set of object-oriented design principles. Loosely, Liskov’s principle states that if Bar
is a subclass of Foo
, then all instances of Foo
should be replaceable by Bar
s without rendering the program incorrect. I had always found this principle the most enigmatic, since none of the classes I was writing seemed to work in this way. My slavish devotion to the DRY principle meant I was making superclasses where only the merest superficial relationship existed between the various subclasses (pretty much any class whose name could never be more descriptive than Base
). Through conversations with co-workers I found I wasn’t alone. Liskov’s substitution principle seemed to be little understood and, as a consequence, rarely followed.
A Substitutable Example
So, what’s an example of a substitutable class hierarchy? Let’s take a look at one of Ruby’s core libraries, which features a lot of well-designed APIs. The BasicSocket
class in the socket
library defines what all sockets can do: send
, recv
, etc. Its subclasses, IPSocket
and UNIXSocket
, don’t tinker with that interface, they simply abstract away a particular protocol implementation for send
and recv
. Further subclasses, like TCPSocket
and UDPSocket,
abstract further protocols – but fundamentally still behave the same way. These classes are substitutable in as much as you can swap any kind of socket for any other kind and, from the point of view of someone sending and receiving data on the socket, your program still works correctly. The power of this is that any object that needs to read from or write to a socket doesn’t need to care about the particular transport mechanism and can just focus on using the socket in a way it cares about, namely the send
and recv
methods. The various socket classes abstract over the messiness of networking while still allowing complete extensibility to define new socket types. Any current consumer of a socket could use one of the new socket classes without any modification, since the classes are substitutable.
Where We Can Use This
In general, subclasses that are substitutable with one another are simply different “flavors” of the same basic thing. All instances respond to the same interface but differ in their internal implementations. Consumers of the objects work with them generically, leaving all the “decisions” to the object itself. This means the only decision a consumer must make when using a substitutable subclass is which constructor to use to get an object that behaves in the desired way. The classic scenario where this kind of relationship crops up is in the ubiquitous User
class in most Rails apps. Since the abilities and permissions of the ever-present current_user
changes how controllers work and the way views are rendered, logic for users is usually splashed haphazardly around a Rails codebase. For example, this example of user logic in a view should look familiar:
<% if current_user['account_type'] == "premium" %>
<%= link_to 'Premium Site', ... %>
<% end %>
This user-logic-in-controller may also be annoyingly familiar:
As your application grows you might find yourself with many different tiers of users with different abilities and permissions, and an associated tangle of conditional logic:
<% if current_user.admin? %>
<%= link_to 'admin site', ... %>
<% elsif current_user.admin? || current_user.pro? || current_user.premium? %>
<%= link_to 'Premium Site', ... %>
<% if !current_user.premium? %>
<%= link_to 'Upgrade to premium!', ... %>
<% end %>
<% elsif current_user.pro_plus? %>
...
<% end %>
Code like this is hard to work with since the consumers ofUser
objects, the controllers and views, need a lot of knowledge about the business logic around users. The poor encapsulation of concerns leaves you with code that’s hard to extend: if you have a new account tier (call it “Pro Plus Platinum”), it would require locating every conditional block like the one above throughout your entire code base and adding a new branch or risk introducing insidious bugs. Often, code like this will be refactored into high-level conditionals such that the logic can be pushed back into the model instead of living in the view. The refactored view might look like this:
<%= link_to_if current_user.can_access_admin_site?, 'Admin', ... %>
<%= link_to_if current_user.can_access_premium_site?, 'Premium Site', ... %>
<%= link_to_if current_user.nag_for_premium_upgrade?, 'Upgrade to Premium!', ... %>
However, all this does is push those gross conditionals into the model:
class User
def can_access_admin_site?
admin?
end
def can_access_premium_site?
admin? || premium?
end
def nag_for_premium_upgrade?
basic? || pro?
end
end
We can refactor the conditionals out by defining subclasses of User
to represent the different account types. Each subclass implements the User
interface as appropriate given its type’s business logic.
class BasicUser < User def can_access_premium_site? false end def can_access_admin_site? false end end class PremiumUser < User def can_access_premium_site? true end def can_access_admin_site? false end end class AdminUser < User def can_access_premium_site? true end def can_access_admin_site? false end end
(Ignore for a moment that these methods probably belong in a presenter and not the model). Since the User
subclasses are all substitutable, controller and view code can work with User
objects generically and no longer need to make any decisions. The only point where your code branches, then, is when setting current_user
to the correct subclass. Rails’ single-table inheritance makes this very simple to do.
The MECE Principle and How to Make Substitutable Classes
So classes that follow Liskov’s substitution principle are pretty rad, but how can we make sure the classes we’re writing are substitutable? Let me introduce the MECE principle, which comes from the world of business consulting. MECE stands for “Mutually Exclusive/Collectively Exhaustive” and refers to an organizational principle to help decompose a situation into a number of smaller independent scenarios that can be handled individually. A set of scenarios is MECE if every situation falls under one and only one of the given scenarios. This kind of organization is very widely applicable. When deciding to use direct inheritance, we want our subclasses to form a MECE collection for instances of that class. Collective exhaustion means that any object that represents a user of the system must belong to some subclass of User
; that is, there shouldn’t be two different classes that both represent users of the system. Mutual exclusion means that any user object that belongs to one subclass doesn’t belong to any other; put another way, given any user object, I should be able to unambiguously categorize it into a unique subclass. Let’s see how this applies to the User
example above. There are many different possible ways to categorize the users of the system. We want to pick an axis that divides the users along lines that are most useful to us. For example, we could categorize every user into a demographic range based on their age: 18-24 go into theYoung
subclass, 25-55 into Middle
and over 55 intoSenior
. This categorization works great if user behavior is primarily driven by demographic range, but it’s a poor match for our example. Since the account tier is what primarily drives the behavior of users, controlling what they’re allowed to access, it’s the perfect axis to base our categorization on. We can see how the division of users into subclasses based on account tier is MECE. Every user belongs to one tier or another, and each tier is mutually exclusive with the other tiers (i.e. a user is either a BasicUser
or aPremiumUser
and not some weird in-betweeny state). When divided up into subclasses like this, each subclass can wholly encasulate the business logic that differentiates it from other subclasses of User
, without having a mess of conditionals or leaking user responsibilities into other objects.
Conclusion
We often see classes that use a lot of conditionals or case statements to change their behavior based on the value of a certain symbol, string, or boolean attribute (how many times have you seen something likeif @file_type == "csv"
?). This is a symptom of multiple classes “superimposed” on one another. By breaking up those kinds of classes into a MECE collection of subclasses, we can cleanly encapsulate business logic, hide implementation details, and keep code extensible, all of which are goals of object-orientend programming.