Background
Switch Case has been around for as long as I remember, was probably introduced to it in C language.
Many developers still use this pattern for alternating the sequence of execution.
While the Switch Case pattern has its own uses in the current world, there are specific scenarios where Switch Case pattern can be replaced with more flexible patterns allowing greater code maintainability, flexibility and readability.
A well thought out migration to the new patterns can also result in conformance to the SOLID principles.
consider the following code sample:
switch (notificationType){
case NotificationTypes.ByDevice:
DeviceAlerterData deviceAlerterData = configurationData as DeverAlerterData;
if (deviceAlerterData!=null){
alertData.Filters = new DeviceFilter{
HostNames = new HashSet<string>(deviceAlerterData.DeviceNames),
FromDate = dateFrom,
ToDate = dateTo,
OriginatingUserId = LoggedInUser.UserId
};
}
break;
case NotificationTypes.ByGroup:
GroupAleterData groupAlerterData = configurationData as GroupAlerterData;
if (groupAlerterData!=null){
alertData.Filters = new DeviceFilter{
GroupsToAlert = groupAlerterData.GroupIds,
FromDate = dateFrom,
ToDate = dateTo,
OriginatingUserId = LoggedInUser.UserId
};
}
break;
}
The Problem
Now imagine adding another notification type to the code, keeping the same pattern intact would mean,
- Duplication of code
- Impacting code readability
- Increase in code length directly impacting complexity matrices.
- Difficult to make changes across the pattern e.g. renaming ToDate to ToAlertDate or adding a new field across all the switch cases.
- Coupling between lots of classes.
The Solution
There are various ways to solve these problems. I will discuss the best approach in my opinion here in this article.
Imagine if all that code is reduced to this:
IAlertFilter alertFilter = AlertFilterFactory.CreateFilterFromNotificationType(notificationType, configurationData);
alertData.ApplyFilters(alertFilter);
By doing the above we have already solved the problem of code readability and depending on other things in happening in the class have achieved S,L, and I part of the SOLID principle.
If you notice we have also addressed the length of code issue as well. The code is going to be much cleaner in this case.
What does the AlertFilterFactory contains?
public static class AlertFilterFactory{
public static IAlertFilter CreateFilterFromNotificationType(NotificationTypes notificationType, ConfigurationData configData){
IAlertFilter alertFilter = null;
if (notificationType==NotificationTypes.ByDevice)
alertFilter = new DeviceNotificationFilter(configData);
else if (notificationType==NotificationTypes.ByGroup)
alertFilter = new GroupNotificationFilter(configData);
else
throw new InvalidOperationException("NotificationType: " + notificationType + " is not supported");
return alertFilter;
}
}
In the above example we make use of the D part of the SOLID principle. Performing dependency injection gives us the ability to scale functionality while following the O part of the SOLID principle.
The IAlertFilter will look something like this:
public interface IAlertFilter{
DeviceFilter GetFilter();
}
Then we create a base class:
public abstract class AlertFilterBase
{
ConfigurationData data;
public AlertFilterBase(ConfigurationData configurationData)
{
data = configurationData;
}
public DeviceFilter CreateDefaultFilter()
{
var filter = new DeviceFilter
{
FromDate = data.DateFrom,
ToDate = data.DateTo,
OriginatingUserId = data.LoggedInUserId
};
return filter;
}
}
Then we implement the classes this way.
public class DeviceNotificationFilter : AlertFilterBase, IAlertFilter
{
public DeviceNotificationFilter(ConfigurationData configurationData) : base(configurationData)
{
}
public DeviceFilter GetFilter()
{
var filter = CreateDefaultFilter();
return filter;
}
}
public class GroupNotificationFilter : AlertFilterBase, IAlertFilter
{
public GroupNotificationFilter(ConfigurationData configurationData) : base(configurationData)
{
}
public DeviceFilter GetFilter()
{
var filter = CreateDefaultFilter();
return filter;
}
}
There we go, there can be other refactoring done based on the real requirements. This refactoring now allows for scaling and tries to comply to SOLID principles while solving problems mentioned above.