Strategy Pattern with each algorithm having a different method signature

Viewed 1738

I am doing a refactor over certain code.

We have a list of investors with amounts assigned to each. The total of amounts should be equal to another total, but sometimes there are a couple of cents of difference, so we use different algorithms to assign these differences to each investor.

The current code is something like this:

public void Round(IList<Investors> investors, Enum algorithm, [here goes a list of many parameters]) {

   // some checks and logic here - OMMITED FOR BREVITY

  // pick method given algorithm Enum

  if (algoritm == Enum.Algorithm1) {
      SomeStaticClass.Algorithm1(investors, remainders, someParameter1, someParameter2, someParameter3, someParameter4)
  } else if (algoritm == Enum.Algorithm2) {
     SomeStaticClass.Algorithm2(investors, remainders, someParameter3)
  }
}

so far we only have two algorithms. I have to implement the third one. I was given the possibility to refactor both existing implementations as well as do some generic code to make this function for future algorithms, maybe custom to each client.

My first thought was "ok, this is a strategy pattern". But the problem I see is that both algorithms receive a different parameter list (except for the first two). And future algorithms can receive a different list of parameters as well. The only thing in "common" is the investor list and the remainders.

How can I design this so I have a cleaner interface? I thought of

  1. Establishing an interface with ALL possible parameters, and share it among all implementations.
  2. Using an object with all possible parameters as properties, and use that generic object as part of the interface. I would have 3 parameters: The list of investors, the remainders object, and a "parameters" object. But in this case, I have a similar problem. To instantiate each object and fill the required properties depends on the algorithm (unless I set all of them). I would have to use a factory (or something) to instantiate it, using all parameters in the interface, am I right? I would be moving the problem of too many parameters to that "factory" or whatever.
  3. Using a dynamic object instead of a statically typed object. Still presents the same problems as before, the instantiation

I also thought of using the Visitor Pattern, but as I understand, that would be the case if I had different algorithms for different entities to use, like, another class of investors. So I don't think it is the right approach.

So far the one that convinces me the most is the second, although I am still a bit reticent about it.

Any ideas?

Thanks

2 Answers

Strategy has different implementations. Its straightforward when all alternate Concrete Strategies require same type signature. But when concrete implementations start asking for different data from Context, we have to gracefully take a step back by relaxing encapsulation ("breaking encapsulation" is known drawback of strategy), either we can pass Context to strategies in method signature or constructor depending upon how much is needed.

By using interfaces and breaking big object trees in to smaller containments we can restrict the access to most of the Context state.

following code demonstrates passing through method parameter.

    public class Context {
        private String name;
        private int id;
        private double salary;
        Strategy strategy;
        void contextInterface(){
            strategy.algorithmInterface(this);
        }
        public String getName() {
            return name;
        }
        public int getId() {
            return id;
        }
        public double getSalary() {
            return salary;
        }
    }

    public interface Strategy {
    // WE CAN NOT DECIDE COMMON SIGNATURE HERE
    // AS ALL IMPLEMENTATIONS REQUIRE DIFF PARAMS
    void algorithmInterface(Context context);
    }

    public class StrategyA implements Strategy{
        @Override
        public void algorithmInterface(Context context) {
            // OBSERVE HERE BREAKING OF ENCAPSULATION 
            // BY OPERATING ON SOMEBODY ELSE'S DATA
            context.getName();
            context.getId();
        }
    }

    public class StrategyB implements Strategy{
        @Override
        public void algorithmInterface(Context context) {
            // OBSERVE HERE BREAKING OF ENCAPSULATION 
            // BY OPERATING ON SOMEBODY ELSE'S DATA
            context.getSalary();
            context.getId();
        }
    }

Okay, I might be going in the wrong direction... but it seems kinda weird that you're passing in arguments to all the algorithms, and the identifier to which algorithm to actually use. Shouldn't the Round() function ideally just get what it needs to operate?

I'm imagining the function that invokes Round() to look something like:

if (something)
    algToUse = Enum.Algorithm1;
else
    if (otherthing)
        algToUse = Enum.Algorithm2;
    else
        algToUse = Enum.Algorithm3;
Round(investors, remainder, algToUse, dayOfMonth, lunarCycle, numberOfGoblinsFound, etc);

... what if, instead, you did something like this:

public abstract class RoundingAlgorithm
{
    public abstract void PerformRounding(IList<Investors> investors, int remainders);
}
public class RoundingRandomly : RoundingAlgorithm
{
    private int someNum;
    private DateTime anotherParam;
    public RoundingRandomly(int someNum, DateTime anotherParam)
    {
        this.someNum = someNum;
        this.anotherParam = anotherParam;
    }
    public override void PerformRounding(IList<Investors> investors, int remainder)
    {
        // ... code ...
    }
}
// ... and other subclasses of RoundingAlgorithm

// ... later on:
public void Round(IList<Investors> investors, RoundingAlgorithm roundingMethodToUse)
{
    // ...your other code (checks, etc)...

    roundingMethodToUse.Round(investors, remainders);
}    

... and then your earlier function simply looks like:

RoundingAlgorithm roundingMethod;
if (something)
    roundingMethod = new RoundingByStreetNum(1, "asdf", DateTime.Now);
else
    if (otherthing)
        roundingMethod = new RoundingWithPrejudice(null);
    else
        roundingMethod = new RoundingDefault(1000);
Round(investors, roundingMethod);

... basically, instead of populating that Enum value, just create a RoundingAlgorithm object and pass that in to Round() instead.

Related