The best way to implement a Main Something property
In this post, we will explore a common design problem: implementing a Main Something property. There’s an equally common solution to this problem which is sub-optimal in most cases.
The best way to implement a Main Something property
Let me explain the design problem first. Imagine you develop a CRM system. It has an entity Customer
and each customer keeps a list of contacts, like this:
public class Customer
{
public string Name { get; private set; }
public IReadOnlyList<Contact> Contacts { get; private set; }
}
public class Contact
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
}
Now let’s say there’s a "go to" contact in every client company - the main person you maintain the relationship with within that company. How would you implement it?
The most common solution I’ve seen to this date is to introduce a flag, like so:
public class Contact
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public bool IsMain { get; set; }
}
Now you can add a calculated property to Customer
which would return the main contact and some method that would update it:
public class Customer
{
public string Name { get; private set; }
public IReadOnlyList<Contact> Contacts { get; private set; }
public Maybe<Contact> MainContact
{
get { return Contacts.SingleOrDefault(x => x.IsMain); }
}
public void UpdateMainContact(Contact contact)
{
if (!Contacts.Contains(contact))
throw new InvalidOperationException();
Maybe<Contact> oldMainContact = MainContact;
if (oldMainContact.HasValue)
{
if (oldMainContact.Value == contact)
return;
oldMainContact.Value.IsMain = false;
}
contact.IsMain = true;
}
}
At this point, you probably recall domains in which you needed to do something alike. It wasn’t necessarily "Main Something". It could also be "Default Something" or similar: the default image in a photo album, the favorite song in a song collection, etc. Every such requirement I encountered had the implementation I brought above: a collection of items, each with a boolean flag.
Alright, so what is wrong with this implementation? Well, nothing, except that there’s a more straightforward alternative.
Instead of relying on the IsMain
flag in the Contact
class, we can make the MainContact
property in Customer
non-calculated:
public class Customer
{
public string Name { get; private set; }
public IReadOnlyList<Contact> Contacts { get; private set; }
public Maybe<Contact> MainContact { get; private set; }
}
The UpdateMainContact
then can be reduced to this version:
public void UpdateMainContact(Contact contact)
{
if (!Contacts.Contains(contact))
throw new InvalidOperationException();
// Maybe supports proper == operator
if (MainContact == contact)
return;
MainContact = contact;
}
And we can remove the IsMain
property:
public class Contact
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
}
Okay, let’s break this alternative down.
At first, it looks like we introduced a redundancy to our Customer
. Instead of N
contacts, we now store N+1
: N
in the Contacts
collection and another one in MainContact
. However, it is not the case. We don’t keep an excessive number of physical contacts. MainContact
is just a reference to one of the entities which already exists under the collection of contacts.
What we did here is we traded the old Contact.IsMain
property for the new Customer.MainContact
one. There are two corollaries from this refactoring.
First, it’s become harder to remove a contact from the collection of contacts as we now need to check whether it also referenced by the new property:
public void RemoveContact(Contact contact)
{
if (!Contacts.Contains(contact))
return;
Contacts.Remove(contact);
if (MainContact == contact)
{
MainContact = null;
}
}
Theoretically, we could run into an inconsistency where MainContact
refers to a contact which no longer exists. That could take place because of a race condition in a multi-threaded scenario or if we just forget to add the if statement like the one above. In the old implementation, we just remove the contact from the collection of contacts and MainContact
updates itself automatically due to its calculated nature.
On the other hand, it’s now easier to change the main contact. The UpdateMainContact
method has become simpler as we don’t need to chase down the existing contacts with IsMain
set to true and reset them anymore:
public void UpdateMainContact(Contact contact)
{
if (!Contacts.Contains(contact))
throw new InvalidOperationException();
// Maybe supports proper == operator
if (MainContact == contact)
return;
MainContact = contact;
}
We just update the MainContact
property itself and that’s it. It’s now also impossible to run into another inconsistency that could potentially show up in the old implementation: having more than one main contact. The absence of the IsMain
property protects us from that.
So, in terms of the ease of maintaining invariants, both solutions pretty much break even. We trade the boolean flag for the additional reference and, as a corollary, one potential inconsistency for the other.
Why am I claiming the latter alternative is more straightforward then? Because of its explicitness. Think about the domain model we have here. Each of our customers needs to maintain a list of contacts and somehow distinguish one of them. This very act of differentiating the main contact is a direct responsibility of the Customer
entity, Contact
itself has nothing to do with it. Pushing this responsibility to Contact
in the form of the IsMain
property messes up the domain model. It inverts the responsibility graph. As a result, the model no longer accurately represents the actual domain, and that adds additional mental overhead when translating between the two.
One of the biggest goals of Domain-Driven Design is to build models that reflect the problem domain as closely as possible, with attention to relevant details and disregard for irrelevant ones. The closer the model represents the problem, the easier it is to reason about it. (Another DDD concept that helps with that is Ubiquitous Language).
Pushing an improper responsibility where it doesn’t belong doesn’t look like a big deal but such things add up. It’s a good idea to eliminate disparities between the domain and its model when you can, and this concrete design problem is a good example of one. Also, it’s just a good practice overall to be explicit in your code.
By the way, it’s not only the application code that you can refactor in the above way, it’s also the database structure. Instead of this:
you can implement the relationship like this:
Conclusion
Having clearly defined responsibilities is preferable in most cases. The Main Something problem is a good example where you can practice it. A non-calculated property instead of IsMain
flag puts domain knowledge to the right place and makes it explicit.
Subscribe
Comments
comments powered by Disqus