The best way to implement a Main Something property

By Vladimir Khorikov

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:

The best way to implement a Main Something property: Implementation with IsMain

Implementation with IsMain

you can implement the relationship like this:

The best way to implement a Main Something property: Explicit alternative

Explicit alternative

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.





  • http://etienne.mermillod.net Mermich

    Hi, on your example we are doing 2 things when we cupdate the main contact:
    – flag the preffered contact
    – update its data
    If we only perfom only one of theses task we don’t have any issue.

  • https://github.com/Mykezero Mykezero

    Using a reference vs a flag not only increases the explicitness of the code, but also increases the code readability.

    The IsMain contact flag caused the code to operate in a very indirect way: the search and update strategies are updating the main contact by setting a flag, instead of doing the work directly.

    By doing the work in an explicit fashion, the code readability is improved greatly as well.

    **Edit: Awesome, you talk about readability in the other article you linked to XD. I’ll need to read that next! Lots of great insights here. Thanks for sharing this ^^

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Yep, exactly! 🙂

  • Shani Fedida

    I Really like your approach. You give concete examples of problems and concrete solutions which we can learn from and apply in other areas. Keep the good work ☺
    I found the title kind of weird before I read the post. I didn’t know what this is about, I thought it is relatef somehow to the main function 😆

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Thanks, glad you liked it! Yeah, naming looks a bit weird indeed 🙂

  • Dmitry Naumov

    Vladimir,
    what do you think about using such method signature?

    public void ChooseMainContact(Func selector)

    Let me explain. Let’s say you can add Contact to Customer and select main one. Then it is become unclear should you first add contact and then set it as main, or when you set it as main it will be automatically added to list of contacts. To avoid this ambiguity, instead of throwing exception, I try to explicitly show that choosing main contact is the process of selection from existing ones.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Hi Dmitry,

      What happens if no contact is found? Or if there are more than 1? Seems that you would still need to throw an exception. I like the naming, though, it explicitly tells the reader that the method chooses a contact, not adds it. But I personally would still pass a contact to it, not a delegate.

      • Dmitry Naumov

        Vladimir,
        the idea is that callee, which is Customer class, calls your delegate until first ‘true’ is returned.
        The situation when there is no main contact exists in either approaches, we have to accept it or, if it is not possible, should more explicitly tell it via API.