Identify if-else statements as a problem in your code.
If-else statements can be problematic if not used correctly. They can be difficult to read and can lead to code that is difficult to maintain. When used incorrectly, if-else statements can also lead to errors. Especially else statements may work unexpectedly when someone else adds another feature that is not fit your if condition. No one wants an application that is not stable or works unexpectedly. Let’s examine the following example that may not be the best example to explain the if/else nested problem. But hopefully, it’ll give you a good guideline as to what the problem is.
const MyButton = ({ theme, rounded, hover, animation, content }) => {
let className = '';
if (theme === 'default') {
className = rounded ? 'default-btn rounded' : 'default-btn';
if (hover) {
className = className + ' hover';
}
} else if (theme === 'primary') {
if (rounded) {
if (hover) {
if (animation) {
className = 'primary-btn rounded hover my-custom-animation';
} else {
className = 'primary-btn rounded hover';
}
} else {
className = 'primary-btn rounded';
}
} else {
if (hover) {
className = 'primary-btn hover';
} else {
className = 'primary-btn';
}
}
}
return (
<button className={className}>{content}</button>
);
}
It is very hard to read and understand, isn’t it? What about adding a new condition or modifying the existing one?
I am pretty sure the code above won’t work as you expect after debugging hundred times. Moreover, it would be very painful to make a simple change in such code because you must understand almost all the logic whenever you want to do changes.
⚠ Problem
Imagine that you’re creating an order management application and you run a transportation logic whenever an order is created.
void CreateOrder(Order order)
{
/*
* Saving order process..
*/
/*
* Transportation proces..
*/
TransportOrder(order);
}
Basically, your code will be similar to the code above.
After a while, your app becomes pretty popular. Each day you receive dozens of orders from other countries and your company makes an agreement with an airways transportation company. So you should implement airways transportation to your code and use it when an order’s distance is more than 1000km.
void CreateOrder(Order order)
{
/*
* Saving order process..
*/
/*
* Transportation proces..
*/
if(order.Distance < 1000) {
TransportOrderByHighway(order);
} else {
TransportOrderByAirway(order);
}
}
Then they realized that this process is expensive if an order’s weight is greater than 100kg and made another agreement with a sea transportation company but if an order is urgent the order should be transported by airway in all conditions.
void CreateOrder(Order order)
{
/*
* Saving order process..
*/
if(order.Distance < 1000 && !order.Urgent) {
TransportOrderByHighway(order);
} else if(order.Urgent || (order.Distance >= 1000 && order.Weight <= 100)) {
TransportOrderByAirway(order);
} else {
TransportOrderBySeaway(order);
}
}
The problem is that you modified previous conditions which absolutely breaks the open-closed principle, some extra condition blocks were added and what will happen when you want to use a railway or add some extra rules? It will probably turn into code like the first example.
✔️ Solution
Let’s start with creating a new dotnet console project with the command written below.
dotnet new console -n order-management
The project will be available on github at last of this blog.
Then, let’s create a pretty simple Order class that fits our problem.
public class Order
{
public int Distance { get; set; }
public bool Urgent { get; set; }
}
We need to create an abstract class (or you can use interfaces and DI with some other approaches) that contains our abstract methods.
public abstract class Transporter : IDisposable
{
public abstract void Transport(Order order);
public void Dispose()
{
// Suppress finalization.
GC.SuppressFinalize(this);
}
}
Now, we have an abstract Transporter class that has a transport method which accepts Order as a parameter. The next step is creating and implementing our business logic to SeaTransporter, HighwayTransporter, and AirwayTransporter classes.
public class SeawayTransporter : Transporter
{
public override void Transport(Order order)
{
Console.WriteLine($"The order transported by SeawayTransporter");
}
}
public class AirwayTransporter : Transporter
{
public override void Transport(Order order)
{
Console.WriteLine($"The order transported by AirwayTransporter");
}
}
public class HighwayTransporter : Transporter
{
public override void Transport(Order order)
{
Console.WriteLine($"The order transported by HighwayTransporter");
}
}
We have three separate classes derived from Transporter and the magic starts exactly at this point. Let’s add an abstract IsSuitableWithOrder method that returns a boolean value and implement conditions to that three classes.
Our code should be similar to the one below at this point.
public abstract class Transporter : IDisposable
{
public abstract void Transport(Order order);
protected abstract bool IsSuitableWithOrder(Order order);
public void Dispose()
{
// Suppress finalization.
GC.SuppressFinalize(this);
}
}
public class SeawayTransporter : Transporter
{
protected override bool IsSuitableWithOrder(Order order)
{
return !order.Urgent && (order.Distance >= 1000 && order.Weight > 100);
}
public override void Transport(Order order)
{
Console.WriteLine($"The order transported by SeawayTransporter");
}
}
public class AirwayTransporter : Transporter
{
protected override bool IsSuitableWithOrder(Order order)
{
return order.Urgent || (order.Distance >= 1000 && order.Weight <= 100);
}
public override void Transport(Order order)
{
Console.WriteLine($"The order transported by AirwayTransporter");
}
}
public class HighwayTransporter : Transporter
{
protected override bool IsSuitableWithOrder(Order order)
{
return !order.Urgent && order.Distance < 1000;
}
public override void Transport(Order order)
{
Console.WriteLine($"The order transported by HighwayTransporter");
}
}
So, we need to use the Transporter class as a factory that returns a suitable transporter class to us but also it must do this without any hardcoded conditions or manual checks in order to be able to add new classes in the feature without modifying previous codes and reflection may help us for doing this.
Let’s start with writing an extension method that returns all derived classes from the given type.
public static class Extensions
{
public static IEnumerable<Type> FindSubClasses(this Type baseType)
{
var assembly = baseType.Assembly;
return assembly.GetTypes().Where(t => t.IsSubclassOf(baseType));
}
}
What we want to do is find all transporter subclasses, execute their IsSuitableWithOrder method and return the suitable one. So let’s do final touches on our abstract Transporter class.
public abstract class Transporter : IDisposable
{
public static Transporter GetTransporter(Order order)
{
var instance = GetSuitableInstance(typeof(Transporter).FindSubClasses(), order);
return instance;
}
private static Transporter GetSuitableInstance(IEnumerable<Type> types, Order order)
{
foreach (var @class in types)
{
try
{
var instance = Activator.CreateInstance(@class) as Transporter;
var isSuitable = instance.IsSuitableWithOrder(order);
if (isSuitable != true)
{
instance.Dispose();
continue;
}
return instance;
}
catch (System.Exception ex)
{
continue;
}
}
throw new NotImplementedException("System can not found any transporter for given order." + order);
}
public abstract void Transport(Order order);
protected abstract bool IsSuitableWithOrder(Order order);
public void Dispose()
{
// Suppress finalization.
GC.SuppressFinalize(this);
}
}
As you see we added a static GetTransporter method that gets all subclasses from assembly and returns one of them that is suitable for the order.
Note that the factory method doesn’t have to create new instances all the time. It can also return existing objects from a cache, an object pool, or another source.
It might be useful if you check the builder pattern and method chaining.
As a final step let's return to the Program.cs file, prepare a list of orders, and transport them.
List<Order> orders = new()
{
new Order
{
Urgent = false,
Distance = 100,
Weight = 50
},
new Order
{
Urgent = false,
Distance = 2000,
Weight = 4000
},
new Order
{
Urgent = false,
Distance = 1100,
Weight = 5
},
new Order
{
Urgent = true,
Distance = 1200,
Weight = 250
}
};
foreach (var order in orders)
{
Console.WriteLine("----------------------");
Console.WriteLine(order.ToString());
Transporter.GetTransporter(order).Transport();
Thread.Sleep(200);
}
Instead of a lot of if conditions all you need is to write the code below.
Transporter.GetTransporter(order).Transport();
Moreover, you can easily add new classes for other options and add new features to existing classes.
And here is the result.
GitHub: https://github.com/edisnezir/factory-pattern-order