48  
bestpractices
Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Sep 28, 2022

How to stop using If-else and make your code more readable

Автор:
Edis Nezir
Источник:
Просмотров:
2452

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

Похожее
Apr 3, 2023
Author: Shubhadeep Chattopadhyay
Clean code is a set of programming practices that emphasize the readability, maintainability, and simplicity of code. Writing clean code is essential because it helps developers to understand and modify code more efficiently, which can save time and reduce the...
Sep 5, 2023
Author: Edson Moisinho
Simplifying data transport in C#. In modern C# development, data transport objects (DTOs) play a crucial role in exchanging information between different layers of an application, such as between a client and a server, and traditionally, developers have used classes...
Aug 8
Author: Anton Martyniuk
Integration testing is a type of software testing essential for validating the interactions between different components of an application, ensuring they work together as expected. The main goal of integration testing is to identify any issues that may arise when...
Feb 2
Author: Achref Hannachi
Introduction LINQ (Language Integrated Query) is a powerful feature in C# that allows developers to perform complex queries on collections and databases using a syntax that is both expressive and readable. However, writing LINQ queries efficiently is essential to ensure...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS
Если вам понравился этот сайт и вы хотите меня поддержать, вы можете
LinkedIn: Sergey Drozdov
Boosty
Donate to support the project
GitHub account
GitHub profile