Поиск  
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
Источник:
Просмотров:
1703

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

Похожее
Mar 18
Author: codezone
File reading operations in C# are crucial for many applications, often requiring efficiency and optimal performance. When handling file reading tasks, employing the right strategies can significantly impact the speed and resource utilization of your application. Here are some best...
Feb 26, 2023
Author: Vitalii Shevchuk
Achieve Typescript mastery with a 21-steps guide, that takes you from Padawan to Obi-Wan.Content Intro Best Practice 1: Strict Type Checking Best Practice 2: Type Inference Best Practice...
Dec 25, 2022
Author: Sannan Malik
This article will look at the differences between static and readonly C# fields. Basically, a static readonly field can never be replaced by an instance of a reference type. In contrast, a const constant object cannot be replaced by a...
Oct 20, 2022
Author: Vishal Yelve
ASP.NET Core MVC is a web development framework, widely used by developers around the word, to develop web applications. These web applications have proven to be vulnerable to attacks from different sources, though, and it is our responsibility to safeguard...
Написать сообщение
Почта
Имя
*Сообщение


© 1999–2024 WebDynamics
1980–... Sergey Drozdov
Area of interests: .NET Framework | .NET Core | C# | ASP.NET | Windows Forms | WPF | HTML5 | CSS3 | jQuery | AJAX | Angular | React | MS SQL Server | Transact-SQL | ADO.NET | Entity Framework | IIS | OOP | OOA | OOD | WCF | WPF | MSMQ | MVC | MVP | MVVM | Design Patterns | Enterprise Architecture | Scrum | Kanban