Home > Net >  Best Practice for OOP function with multiple possible control flows
Best Practice for OOP function with multiple possible control flows

Time:09-21

In my project, I have this special function that does needs to evaluate the following:

  1. State -- represented by an enum -- and there are about 6 different states
  2. Left Argument
  3. Right Argument

Left and Right arguments are represented by strings, but their values can be the following:

  1. "_" (a wildcard)
  2. "1" (an integer string)
  3. "abc" (a normal string)

So as you can see, to cover all every single possibility, there's about 2 * 3 * 6 = 36 different logics to evaluate and of course, using if-else in one giant function will not be feasible at all. I have encapsulated the above 3 input into an object that I'll pass to my function.

How would one try to use OOP to solve this. Would it make sense to have 6 different subclasses of the main State class with an evaluate() method, and then in their respective methods, I have if else statements to check:

  • if left & right arg are wildcards, do something
  • if left is number, right is string, do something else
  • Repeat for all the valid combinations in each State subclass

This feels like the right direction, but it also feels like theres alot of duplicate logic (for example check if both args are wildcards, or both strings etc.) for all 6 subclasses. Then my thought is to abstract it abit more and make another subclass:

For each state subclass, I have stateWithTwoWildCards, statewithTwoString etc.

But I feel like this is going way overboard and over-engineering and being "too" specific (I get that this technically adheres tightly to SOLID, especially SRP and OCP concepts). Any thoughts on this?

CodePudding user response:

Possibly something like template method pattern can be useful in this case. I.e. you will encapsulate all the checking logic in the base State.evaluate method and create several methods which subclasses will override. Something along this lines:

class StateBase
   def evaluate():
       if(bothWildcards)
          evalBothWildcards()
       else if(bothStrings)
          evalBothStrings()
       else if ...
   
   def evalBothWildcards():
      ...
   def evalBothStrings():
      ...

Where evalBothWildcards, evalBothStrings, etc. will be overloaded in inheritors.

CodePudding user response:

If you have a lot if else statements, it is possible to use Chain of Responsibility pattern. As wiki says about Chain of Responsibility:

The chain-of-responsibility pattern is a behavioral design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain. A mechanism also exists for adding new processing objects to the end of this chain

So let's dive in code. Let me show an example via C#.

So this is our Argument class which has Left and Right operands:

public class Arguments
{
    public string Left { get; private set; }

    public string Right { get; private set; }

    public MyState MyState { get; private set; }

    public MyKey MyKey => new MyKey(MyState, Left);

    public Arguments(string left, string right, MyState myState)
    {
        Left = left;
        Right = right;
        MyState = myState;
    }
}

And this is your 6 states:

public enum MyState
{
    One, Two, Three, Four, Five, Six
}

This is start of Decorator pattern. This is an abstraction of StateHandler which defines behaviour to to set next handler:

public abstract class StateHandler
{
    public abstract MyState State { get; }

    private StateHandler _nextStateHandler;

    public void SetSuccessor(StateHandler nextStateHandler)
    {
        _nextStateHandler = nextStateHandler;
    }

    public virtual IDifferentLogicStrategy Execute(Arguments arguments)
    {
        if (_nextStateHandler != null)
            return _nextStateHandler.Execute(arguments);

        return null;
    }
}

and its concrete implementations of StateHandler:

public class OneStateHandler : StateHandler 
{
    public override MyState State => MyState.One;

    public override IDifferentLogicStrategy Execute(Arguments arguments)
    {   
        if (arguments.MyState == State)
            return new StrategyStateFactory().GetInstanceByMyKey(arguments.MyKey);
        
        return base.Execute(arguments);
    }
}

public class TwoStateHandler : StateHandler
{
    public override MyState State => MyState.Two;

    public override IDifferentLogicStrategy Execute(Arguments arguments)
    {
        if (arguments.MyState == State)
            return new StrategyStateFactory().GetInstanceByMyKey(arguments.MyKey);

        return base.Execute(arguments);
    }
}

and the third state handler looks like this:

public class ThreeStateHandler : StateHandler
{
    public override MyState State => MyState.Three;

    public override IDifferentLogicStrategy Execute(Arguments arguments)
    {
        if (arguments.MyState == State)
            return new StrategyStateFactory().GetInstanceByMyKey(arguments.MyKey);

        return base.Execute(arguments);
    }
}

Let's pay attention to the following row of code:

return new StrategyStateFactory().GetInstanceByMyKey(arguments.MyKey);

The above code is an example of using Strategy pattern. We have different ways or strategies to handle your cases. Let me show a code of strategies of evaluation of your expressions.

This is an abstraction of strategy:

public interface IDifferentLogicStrategy
{
    string Evaluate(Arguments arguments);
}

And its concrete implementations:

public class StrategyWildCardStateOne : IDifferentLogicStrategy
{
    public string Evaluate(Arguments arguments)
    {
        // your logic here to evaluate "_" (a wildcard)
        return "StrategyWildCardStateOne";
    }
}

public class StrategyIntegerStringStateOne : IDifferentLogicStrategy
{
    public string Evaluate(Arguments arguments)
    {
        // your logic here to evaluate "1" (an integer string)
        return "StrategyIntegerStringStateOne";
    }
}

And the third strategy:

public class StrategyNormalStringStateOne : IDifferentLogicStrategy
{
    public string Evaluate(Arguments arguments)
    {
        // your logic here to evaluate "abc" (a normal string)
        return "StrategyNormalStringStateOne";
    }
}

We need a place where we can store strategies by state and argument value. At first, let's create MyKey struct. It will have help us to differentiate State and arguments:

public struct MyKey
{
    public readonly MyState MyState { get; }

    public readonly string ArgumentValue { get; } // your three cases: "_", 
        // an integer string, a normal string

    public MyKey(MyState myState, string argumentValue)
    {
        MyState = myState;
        ArgumentValue = argumentValue;
    }
    
    public override bool Equals([NotNullWhen(true)] object? obj)
    {
        return obj is MyKey mys
            && mys.MyState == MyState
            && mys.ArgumentValue == ArgumentValue;
    }

    public override int GetHashCode()
    {
        unchecked // Overflow is fine, just wrap
        {
            int hash = 17;
            hash = hash * 23   MyState.GetHashCode();
            hash = hash * 23   ArgumentValue.GetHashCode();
            return hash;
        }
    }
}

and then we can create a simple factory:

public class StrategyStateFactory 
{
    private Dictionary<MyKey, IDifferentLogicStrategy> 
        _differentLogicStrategyByStateAndValue = 
            new Dictionary<MyKey, IDifferentLogicStrategy>()
    {
        { new MyKey(MyState.One, "_"), new StrategyWildCardStateOne() },
        { new MyKey(MyState.One, "intString"), 
            new StrategyIntegerStringStateOne() },
        { new MyKey(MyState.One, "normalString"), 
            new StrategyNormalStringStateOne() }
    };

    public IDifferentLogicStrategy GetInstanceByMyKey(MyKey myKey) 
    {
        return _differentLogicStrategyByStateAndValue[myKey];
    }
}

So we've written our strategies and we've stored these strategies in simple factory StrategyStateFactory.

Then we need to check the above implementation:

StateHandler chain = new OneStateHandler();
StateHandler secondVehicleHandler = new TwoStateHandler();
StateHandler thirdVehicleHandler = new ThreeStateHandler();

chain.SetSuccessor(secondVehicleHandler);
secondVehicleHandler.SetSuccessor(thirdVehicleHandler);

Arguments arguments = new Arguments("_", "_", MyState.One);

IDifferentLogicStrategy differentLogicStrategy = chain.Execute(arguments);
string evaluatedResult = differentLogicStrategy.Evaluate(arguments); // output: 
    // "StrategyWildCardStateOne"

It is just a sketch, but I believe I gave basic idea how it can be done.

  • Related