I am working on a project where a user can send email to either a contact from his directory (Internal) or type an email address explicitly (External Contact) or a mix of both. We also show the read stats of the mail (who read what and how many times) this object somewhat looks as follow

class X {
    String text;
    int count;
    <Contact>
}

In the above domain, the text is the content that was sent, count is the number of how many time contact read it and the would be the Contact who read it.

The contact can be Internal or External (two different objects).

Could someone help me that how to model it in a clean way?

I could have a superclass to Internal and External and use the superclass in class X but I was a bit reluctant in doing that because Internal and External have only one field in common (i.e emailAddress because for external contact we do not have any other field).

Another way to have both the Internal and External Contact field but that would be not clean as at a time only one Object would be present.

Please let me know any clean way to model this relationship.

  • 2
    Is it particularly important to know if the contact was an internal or external contact? Why would you need this information? What would you use it for? – Neil Oct 11 at 7:57
  • Exactly - corporate email almost always includes "contacts" functionality, but once you've picked a recipient, it is usually resolved to a vanilla email address and stored in the message like any address. Is it necessary to preserve the fact that picking the recipient used the address book? – Kilian Foth Oct 11 at 8:02
  • So both Internal and External contact are two different disparate objects with the only common thing as the email address. The object that I mentioned in the question is a model that is used by UI to render different UI elements. For example, if I send Internal contact then the name will be shown else email address will be shown. Moreover, the color of the element would also change based on the type. – Prateek Shah Oct 11 at 10:30

How can explicit choice be modelled?

If you need to model a choice between two completely different types, most object-oriented languages do not offer a built-in way to model that choice (such as tagged unions or sum types). Instead, you can use two nullable variables of different types, and ensure that exactly one of those is set. For example:

class Message {
  private final String text;
  private final InternalContact internalContact;
  private final ExternalContact externalContact;

  private Message(String text, InternalContact internalContact, ExternalContact externalContact) {
    // only one of the contacts may be set:
    // (the "==" operator behaves as "not xor")
    if ((internalContact == null) == (externalContact == null)) {
      throw IllegalArgumentException("either internalContact xor externalContact must be set!");
    }
    this.text = text;
    this.internalContact = internalContact;
    this.externalContact = externalContact;
  }

  public static Message toInternalContact(String text, InternalContact contact) {
    return new Message(text, contact, null);
  }

  public static Message toExternalContact(String text, ExternalContact contact) {
    return new Message(text, null, contact);
  }

  ...
}

Used as: Message m = Message.toExternalContact("hello", externalContact).

Such a design allows the two contact types to be completely unrelated, but any code using your Message must be able to deal with both.

How can interfaces abstract over a choice?

If you only need to perform specific actions with these contacts, you can describe those action in an interface instead:

interface MessageContact {
  String getEmailAddress();
  String getDisplayName();
  boolean isTrusted();
}

This simplifies our Message class:

class Message {
  private final String text;
  private final MessageContact contact;

  public Message(String text, MessageContact contact) {
    this.text = text;
    this.contact = contact;
  }

  ...
}

To create a message, we must wrap the internal or external contacts into the MessageContact interface, which we can do with the Object Adapter Pattern. Here are adapater classes to that interface:

class ExternalMessageContact implements MessageContact {
  private final ExternalContact contact;

  public ExternalMessageContact(ExternalContact c) { this.contact = c; }

  @Override
  String getEmailAddress() { return contact.getAddress(); }

  @Override
  /// Display name of external contacts is their email address.
  String getDisplayName() { return contact.getAddress(); }

  @Override
  boolean isTrusted() { return false; }
}

class InternalMessageContact implements MessageContact {
  private final InternalContact contact;

  public InternalMessageContact(InternalContact c) { this.contact = c; }

  @Override
  String getEmailAddress() { return contact.getAddress(); }

  @Override
  String getDisplayName() { return contact.getFullName(); }

  @Override
  boolean isTrusted() { return true; }
}

Used as: Message m = new Message("hello", new ExternalMessageContact(externalMessage)).

Of course, you can also write static helpers that offer a more convenient interface than explicitly calling the adapter constructors, as with the

class Message {
  ...
  public static Message toExternalContact(String text, ExternalContact contact) {
    return new Message(text, new ExternalMessageContact(contact));
  }
}

This would then offer the same construction interface as in the first example.

Alternatively, the ExternalContact and InternalContact classes could implement the MessageInterface directly, without involving any adapters. Whether to do this depends on whether you want your contact classes to have a direct dependency on the MessageContact interface.

Which approach to model choices is “better”?

Both of these approaches can model a choice, but they are radically different. By introducing an interface, code using the Message cannot differentiate between the concrete Contact classes, except as through the methods provided by the MessageContact interface. The concrete type has been “erased”.

This interface abstraction may or may not be desirable. Whether modelling the choice directly or using an interface is correct depends on how the contact information will be used, and how the system may evolve in the future. For example:

  • When you add new functionality to the InternalContact class, this will be immediately available to consumers of the explicit-choice Message. But with the interface-abstraction Message, the interface hides that behaviour. And you cannot add a new method to the interface without breaking the existing adapters.

  • When you add a new Contact type, the explicit-choice message does not account for this type so the new contact type cannot be used directly. You would have to update the Message class and any code that uses the Message class to consider a third option. In contrast, adding a new contact type with the interface-abstraction Message variant is as easy as adding another adapter.

Both variants involve making a choice “do I have an external or internal contact”, but with the interface this is encapsulated into the adapter class, and the choice is made implicitly by dynamic method dispatch.

The difference between these approaches is related to the Open/Closed Principle. E.g. the interface variant is open for extension in the sense that the system may be extended with new contact types, without having to modify consumers of the Message class. However, if this code is not part of a “public” interface (e.g. a library that is consumed by different teams) then the Open/Closed principle is doesn't matter – you should choose the simplest solution that works. This may still be the interface variant because it offers a much more convenient interface to work with, and is potentially easier to test.

  • Thanks @amon a lot for such a comprehensive answer. Much appreciated. – Prateek Shah Oct 13 at 12:19

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.