Enums Aren't Evil. Conditional Statements Everywhere Are Code Opinion
Enums aren’t evil. Conditional statements everywhere are - CodeOpinion #
Excerpt #
Conditional statements (if/switch) against enums littered everywhere? There are alternative solutions, but it depends on your context.
Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
Are you seeing the same conditional statements (if/switch) against enums littered everywhere? There are different ways of handling that, but a lot of it comes down to your context. Conditionals around type checking, extension methods, Inheritance, and Polymorphism are all options.
YouTube #
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
[]( https://www.youtube.com/watch?v=QoK7jSZ-viw “Play video “Enums aren’t evil. Conditionals everywhere are””)
The original examples of this problem come from a video by Nick Cosentino, which he tagged me in. The gist is that there are a lot of conditional, in this case âif statements,â throughout a codebase and enums being passed around deeper through the call stack.
In the example below, the OfferrngType of a Product is used to determine whether it should have a downloadable URL and filename. However, since only templates, ebooks, and offline courses support this, other types, such as books or courses, will return null.
public enum OfferingType | |
{ | |
Course, | |
Ebook, | |
Book, | |
Template, | |
OfflineCourse, | |
} | |
public sealed record Product(int Id, OfferingType Type); | |
public sealed class ProductHandler(ResourcesHelper _resourcesHelper) | |
{ | |
public void DoStuff(Product product) | |
{ | |
if (product.Type == OfferingType.Template || | |
product.Type == OfferingType.Ebook || | |
product.Type == OfferingType.OfflineCourse) | |
{ | |
var fileName = _resourcesHelper.GetDefaultDownloadFileName(product.Type); | |
var downloadUrl = _resourcesHelper.GetDownloadUrl(product.Type); | |
} | |
} | |
} | |
public sealed class ResourcesHelper | |
{ | |
public string? GetDownloadUrl(OfferingType offeringType) | |
{ | |
if (offeringType == OfferingType.Template || | |
offeringType == OfferingType.Ebook || | |
offeringType == OfferingType.OfflineCourse) | |
{ | |
// some code to do this... | |
return "TODO: get the download URL"; | |
} | |
return null; | |
} | |
public string? GetDefaultDownloadFileName(OfferingType offeringType) | |
{ | |
if (offeringType == OfferingType.Template || | |
offeringType == OfferingType.Ebook || | |
offeringType == OfferingType.OfflineCourse) | |
{ | |
// some code to do this... | |
return "TODO: get the file name"; | |
} | |
return null; | |
} | |
} |
As mentioned, The OfferingType is passed along as an argument to other methods, which ultimately end up doing the exact same conditional check.
A solution in Nickâs video is not to have the conditionals but instead query a database for that information. If there are no records, then return null.
public sealed record DownloadableResource( | |
int Id, | |
int ProductId, | |
string DownloadUrl, | |
string DefaultDownloadFilename); | |
public sealed class ProductHandler2(ResourcesHelper2 _resourcesHelper) | |
{ | |
public void DoStuff(Product product) | |
{ | |
var downloadableResource = _resourcesHelper.GetDownloadable(product.Id); | |
// do stuff with downloadable resource | |
} | |
} | |
public sealed class ResourcesHelper2 | |
{ | |
public DownloadableResource? GetDownloadable(int productId) | |
{ | |
// TODO: go fetch this from the DB or return null... | |
} | |
} |
Runtime #
The solution from Nickâs video above is moving the conditional from in-code at design time (programming) to runtime. The potential problem with this is unnecessary database calls (I/O). It depends on our context. If we have 1000s of products, but only a handful have a download URL, then weâre making many useless DB calls.
Types, Inheritance, Polymorphism, Extensions #
Another option is using different types to represent a specific product type rather than an enum. So we have an abstract Product class and other classes that inherit it, such as Template, Ebook, and OfflineCourse.
The problem I have with this is that itâs really not much different from enums, as we still have to do a conditional check, just not on the enum but rather on type checking.
public abstract class Product(int id) | |
{ | |
public int Id { get; } = id; | |
} | |
public class Template(int id) : Product(id) { } | |
public class Ebook(int id) : Product(id) { } | |
public class OfflineCourse(int id) : Product(id) { } | |
public sealed class ProductHandler(ResourcesHelper resourcesHelper) | |
{ | |
public void DoStuff(Product product) | |
{ | |
if (product is Template || | |
product is Ebook|| | |
product is OfflineCourse) | |
{ | |
var fileName = resourcesHelper.GetDefaultDownloadFileName(product); | |
var downloadUrl = resourcesHelper.GetDownloadUrl(product); | |
} | |
} | |
} | |
public sealed class ResourcesHelper | |
{ | |
public string? GetDownloadUrl(Product product) | |
{ | |
if (product is Template || | |
product is Ebook|| | |
product is OfflineCourse) | |
{ | |
// some code to do this... | |
return "TODO: get the download URL"; | |
} | |
return null; | |
} | |
public string? GetDefaultDownloadFileName(Product product) | |
{ | |
if (product is Template || | |
product is Ebook|| | |
product is OfflineCourse) | |
{ | |
// some code to do this... | |
return "TODO: get the file name"; | |
} | |
return null; | |
} | |
} |
We can take this a bit further and instead define a base class that has a few virtual methods that we can override. More importantly, we can use an option type as the return value or none.
public class Product(int id, OfferingType type) | |
{ | |
public virtual Option<string> GetDownloadUrl() | |
{ | |
return Option.None<string>(); | |
} | |
public virtual Option<string> GetDefaultDownloadFileName() | |
{ | |
return Option.None<string>(); | |
} | |
} | |
public class DownloadProduct(int id, OfferingType type) : Product(id, type) | |
{ | |
public override Option<string> GetDefaultDownloadFileName() | |
{ | |
return Option.Some("Some Value"); | |
} | |
public override Option<string> GetDownloadUrl() | |
{ | |
return Option.Some("Some Value"); | |
} | |
} | |
public class Course(int id, OfferingType type) : Product(id, type) | |
{ | |
public override Option<string> GetDefaultDownloadFileName() | |
{ | |
return Option.None<string>(); | |
} | |
public override Option<string> GetDownloadUrl() | |
{ | |
return Option.None<string>(); | |
} | |
} | |
public sealed class ProductHandler() | |
{ | |
public void DoStuff(Product product) | |
{ | |
product.GetDefaultDownloadFileName().MatchSome( | |
filename => | |
{ | |
// Do something with the filename. | |
}); | |
product.GetDownloadUrl().MatchSome( | |
url => | |
{ | |
// Do something with the url | |
}); | |
} | |
} |
Weâve removed the conditionals and are handling the Option type when there is a value with calling Match(). While not the same, at all, you could make the return type a nullable string and deal with a nullable.
We can keep taking this further and make an abstract class of a product as we did before, but we donât need to override anything; instead, we explicitly have a type that implements using a downloadable product.
public abstract class Product(int id, OfferingType type) { } | |
public class DownloadableProduct(int id, OfferingType type) : Product(id, type) | |
{ | |
public Option<string> GetDefaultDownloadFileName() | |
{ | |
return Option.Some("Some Value"); | |
} | |
public Option<string> GetDownloadUrl() | |
{ | |
return Option.Some("Some Value"); | |
} | |
} | |
public sealed class ProductHandler() | |
{ | |
public void DoStuff(DownloadableProduct product) | |
{ | |
product.GetDefaultDownloadFileName().MatchSome( | |
filename => | |
{ | |
// Do something with the filename. | |
}); | |
product.GetDownloadUrl().MatchSome( | |
url => | |
{ | |
// Do something with the url | |
}); | |
} | |
} |
We arenât handling a base type; weâre explicitly handling a DownloadableProduct. We would have other handlers, or any other code would have different code paths for different offerings.
Or we can go back with our initial conditionals but instead just use an extension method on our enum to group all of the valid offering types to simply our conditional checks.
public static class Extensions | |
{ | |
public static bool IsDownloadable(this OfferingType offeringType) | |
{ | |
var validOfferings = new List<OfferingType>() | |
{ | |
OfferingType.Template, | |
OfferingType.Ebook, | |
OfferingType.OfflineCourse | |
}; | |
return validOfferings.Contains(offeringType); | |
} | |
} | |
public sealed class ProductHandler(ResourcesHelper resourcesHelper) | |
{ | |
public void DoStuff(Product product) | |
{ | |
if (product.Type.IsDownloadable()) | |
{ | |
var fileName = resourcesHelper.GetDefaultDownloadFileName(product.Type); | |
var downloadUrl = resourcesHelper.GetDownloadUrl(product.Type); | |
} | |
} | |
} |
Conditional Statements #
You donât have to have the same repetitive conditions statements against enums. There are many different ways of dealing with them as an alternative. Which solution works best is dependent on your situation. Could you delegate it to runtime and are fine with the additional I/O DB call? Maybe that will work. Maybe that will be terrible system performance.
Maybe youâd rather define explicit types and handle them on their own path rather than try to deal with them all together. If you were to make a Venn diagram, how similar are the two concepts, an offering in this example? Sometimes what we think are the same thing arenât at all and should be explicitly modeled as unique things based on what the capabilities are around them.
Join CodeOpinon!
Developer-level members of myÂ
Patreon orÂ
YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out myÂ
Patreon orÂ
YouTube Membership for more info.
- 5 Tips for Building Resilient Architecture
- The Bulkhead Pattern: How To Make Your System Fault-tolerant