As discussed in the Open-Closed Principle article, the final solution included an EmailSender class, that acted as the orchestrator, and required as dependencies a CurrentDayProvider and an EmailSenderFactory.
The EmailSender class looked like this:
public class EmailSender
{
private readonly ICurrentDayProvider currentDayProvider;
private readonly IEmailSenderFactory emailSenderFactory;
public EmailSender(ICurrentDayProvider currentDayProvider, IEmailSenderFactory emailSenderFactory)
{
this.currentDayProvider = currentDayProvider;
this.emailSenderFactory = emailSenderFactory;
}
public void SendEmail()
{
var day = currentDayProvider.GetCurrentDay();
var emailSender = emailSenderFactory.CreateInternalEmailSender(day);
emailSender.Send();
}
}
The CurrentDayProvider was not the most complicated class ever:
public class CurrentDayProvider : ICurrentDayProvider
{
public DayOfWeek GetCurrentDay() => DateTime.Now.DayOfWeek;
}
And the EmailSenderFactory was pretty standard containing the decision logic based on the provided day of the week:
public class EmailSenderFactory : IEmailSenderFactory
{
public ISendEmail CreateInternalEmailSender(DayOfWeek day)
{
ISendEmail emailSender = day switch
{
DayOfWeek.Monday => new FlyingPigeonEmailSender(),
DayOfWeek.Tuesday => new FlyingCarpetEmailSender(),
// DayOfWeek.Wednesday => ...
// DayOfWeek.Thursday => ...
// DayOfWeek.Friday => ...
// DayOfWeek.Saturday => ...
// DayOfWeek.Sunday => ...
_ => new SmtpEmailSender()
};
return emailSender;
}
}
The interesting aspect in this code is that the CurrentDayProvider and the EmailSenderFactory form a “logical unit”, or a bonded pair. They share a contract.
What one delivers the other consumes (takes as input). The CurrentDayProvider delivers the current day of the week (an enum) and the EmailSenderFactory “coincidentally” needs the same input. If we take a step back and squint we can generalize this “pattern” in the sense that if we would have some sort of discriminator provider that outputs a discriminator and a factory that would consume that discriminator we could use (compose) this pair in order to basically obtain an email sender instance from the current day, sorry I meant the discriminator.
There only one little problem. There is nothing preventing us from creating a discriminator provider that outputs a discriminator of type A and have a factory that needs a discriminator of type B. The question then is how do we keep these two components of this pair in sync (in terms of their types)?
Simple. We define two interfaces, one for the discriminator provider…
public interface IDiscriminatorProvider<T>
{
public T GetDiscriminator();
}
…and one for the email sender factory.
public interface IEmailSenderFactory<T>
{
public ISendEmail CreateEmailSender<T>(T discriminator);
}
We are not concerned here how the factory uses the discriminator to decide on the type of the email sender. That’s beyond the scope of this article.
Now our EmailSender orchestrator class will look like this:
public class EmailSender<T>
{
private readonly IDiscriminatorProvider<T> discriminatorProvider;
private readonly IEmailSenderFactory<T> emailSenderFactory;
public EmailSender(IDiscriminatorProvider<T> discriminatorProvider, IEmailSenderFactory<T> emailSenderFactory)
{
this.discriminatorProvider = discriminatorProvider;
this.emailSenderFactory = emailSenderFactory;
}
public void SendEmail()
{
var discriminator = discriminatorProvider.GetDiscriminator();
var emailSender = emailSenderFactory.CreateEmailSender(discriminator);
emailSender.Send();
}
}
Notice now that the EmailSender needs to be generic in order to generically constrain the type that the discriminator provider outputs and the factory consumes. This way the pair of classes has to stay in sync in terms of the discriminator that is exchanged, otherwise the could would not even compile.
This is an example of using the type system to our advantage, to make the code more expressive and to eliminate a whole class of issues that would have arisen had we not put any constraint in place. The ones responsible of making sure that the discriminator provider and the factory align on the type of the discriminator object would have been the developers. And we know programmers are not the most responsible people in the universe.