29

I came across a problem when using the strategy pattern. I am implementing a service for creating tasks. This service also resolves the responsible clerk for this task. Resolving the clerk is done by using the strategy pattern because there are different ways of doing this. The point is that every strategy could need different parameters to resolve the clerk.

For example:

interface ClerkResolver {
    String resolveClerk(String department);
}

class DefaultClerkResolver implements ClerkResolver {

    public String resolveClerk(String department) {
        // some stuff
    }
}

class CountryClerkResolver implements ClerkResolver {

    public String resolveClerk(String department) {
        // I do not need the department name here. What I need is the country.
    }

}

The problem is that every resolver may depend on different parameters to resolve the responsible clerk. For me this sounds like a design issue in my code. I also tried to have a class as a parameter to keep all values that could be needed by the strategies, like:

class StrategyParameter {

   private String department;
   private String country;

   public String getDepartment() ...
}

interface ClerkResolver {
    String resolveClerk(StrategyParameter strategyParameter);
}

But to be honest, I am not satisfied with this solution because I have to change the parameter class everytime a strategy needs a new / different argument. And secondly the caller of the strategy must set all parameters because he does not know which strategy will resolve the clerk, therefore he has to provide all parameters (but this isn't that bad).

Again, for me this sounds like a design issue in my code, but I can't find a better solution.

--- EDIT

The main problem with this solution is when creating the task. The task service looks like this:

class TaskService {

    private List<ClerkResolver> clerkResolvers;

    Task createTask(StrategyParamter ...) {

        // some stuff

       for(ClerkResolver clerkResolver : clerkResolvers) {
          String clerk = clerkResolver.resolveClerk(StrategyParameter...)
          ...
       }

       // some other stuff
    }

}

As you can see when the TaskService is used, the caller must provide the necessary information to resolve the clerk, i.e. the department name and/or the country, because the TaskService itself doesn't have these information.

When a task has to be created, the caller must provide the StrategyParameter, because they are necessary to resolve the clerk. Again, the problem is, that the caller doesn't have all the information, i.e. he has no knowledge of the country. He can only set the department name. That's why I added a second method to the interface to ensure that the strategy can handle the clerk resolution:

interface ClerkResolver {
    String resolveClerk(StrategyParameter strategyParameter);
    boolean canHandle(StrategyParameter strategyParameter);
}

At the risk of repeating me, this solution doesn't sound right to me.

So, if anybody has a better solution for this problem I would appreciate to hear it.

Thanks for your comments!

4

5 回答 5

15

I think there is some confusion about what the task actually is. In my thinking a task is something that is done by a clerk. So you are able to create a task itself without knowing about a clerk.

Based on that task you can choose an appropriate clerk for it. The assignment of the task to the clerk can itself be wrapped to some other kind of task. So a common interface for choosing a clerk would be:

interface ClerkResolver {
    String resolveClerk(Task task);
}

For implementing this kind of clerk resolver you can use the strategy pattern based on the actual type of the task for example.

于 2013-11-14T09:53:05.107 回答
4

Congratulations, you discovered one of the shortcomings of strategy pattern:

The strategy pattern can be used to host different algorithms which either have no parameters or the set of parameters for each algorithm is the same. However, it falls short if various algorithms with different sets of parameters are to be used.

Luckily, this paper presents an elegant solution:

enter image description here


enter image description here


Applying it to your specific situation:

public abstract class ClerkResolver {  // Role: Algorithm 

    protected Parameter[] parameters;

    public Parameter[] getParameters() {
        return parameters.clone();
    }

    abstract String resolveClerk();

}
class CountryClerkResolver extends ClerkResolver {

    public CountryClerkResolver() {
        parameters = new Parameter[1];
        parameters[0] = new StringParameter("country", "Denmark"); // Default value is 'Denmark'
    }

    private String country;

    @Override
    String resolveClerk() {
        country = ((StringParameter) parameters[0]).getValue();

        // CountryClerkResolver specific code

        return country;
    }

}
class DefaultClerkResolver extends ClerkResolver { // Role: ConcreteAlgorithm

    public DefaultClerkResolver() {
        parameters = new Parameter[1];
        parameters[0] = new StringParameter("department", "someName");
    }

    private String department;

    @Override
    public String resolveClerk() {
        department = ((StringParameter) parameters[0]).getValue();

        // DefaultClerkResolver specific code

        return department;
    }

}
public abstract class Parameter { // Role: Parameter

    private String name;

    public String getName() {
        return name;
    }

    public Parameter(String name) {
        this.name = name;
    }

}
public class StringParameter extends Parameter { // Role: ConcreteParameter

    private String value;

    public StringParameter(String name, String value) {
        super(name);
        this.value = value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

Example use:

public class Main {
    public static void main(String... args) {  // Role: client
        ClerkResolver clerk_1 = new CountryClerkResolver();

        Parameter[] parameters = clerk_1.getParameters();

        StringParameter country = (StringParameter) parameters[0];  // [¤]
        country.setValue("USA"); // Overwriting default value

        clerk_1.resolveClerk();
    }
}

Here is what you would do if you wanted CountryClerkResolver to take e.g. three parameters instead (one of which is an integer):

First introduce an IntegerParameter.

public class IntegerParameter extends Parameter {

    private int value;

    public IntegerParameter(String name, int value) {
        super(name);
        this.value = value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

Now alter the constructor and the method of the strategy:

class CountryClerkResolver extends ClerkResolver {

    public CountryClerkResolver() {
        parameters = new Parameter[1];
        parameters[0] = new StringParameter( "country",         "Denmark"   ); // Default value is 'Denmark'
        parameters[1] = new StringParameter( "newStringParam",  "defaultVal");
        parameters[2] = new IntegerParameter("newIntegerParam", 9999        );
    }

    private String country;
    private String newStringParam;
    private int    newIntegerParam;

    @Override
    String resolveClerk() {
        country         = ((StringParameter)  parameters[0]).getValue();
        newStringParam  = ((StringParameter)  parameters[1]).getValue();
        newIntegerParam = ((IntegerParameter) parameters[2]).getValue();

        // CountryClerkResolver specific code

        return country;
    }

}

For a more detailed explanation of the pattern consult the paper.


Benefits:

  • [Flexible] Change by addition whenever you want to add a new concrete Algorithmor Parameter.
  • You don't have to deal with signatures of the public methods of the algorithm (Strategy) since it doesn't take any parameters; the parameters are to be sat prior to calling the method instead.

Liabilities:

  • [Stability] When fetching the parameters (see [¤]), the programmer might mix up the indexes of the parameters array. (e.g. what if parameters[0] wasn't country but, say, continent)

    • A possible solution to address the stability concern, though at the cost of analyzability, is:
public class Main {
    public static void main(String... args) {  // Role: client
        ClerkResolver clerk_1 = new CountryClerkResolver();

        Parameter[] parameters = clerk_1.getParameters();
                         
                   // Analyzability suffers because of ugly casting: 
        StringParameter country = (StringParameter) getParameterWithName("country", parameters);
        country.setValue("USA"); // Overwriting default value

        clerk_1.resolveClerk();
    }

    private static Parameter getParameterWithName(String paramName, Parameter[] parameters) {
        for (Parameter param : parameters) 
            if (param.getName().equals(paramName))
                return param;
        throw new RuntimeException();  
    }

}

      • To increase readability, an abstraction for the Parameter[] can be introduced:
import java.util.ArrayList;
import java.util.List;

public class ParameterList {

    private final List<Parameter> parameters;

    public ParameterList(int length) {
        this.parameters = new ArrayList<>(length);
    }
    
    public void add(Parameter p) {
        parameters.add(p);
    }
    
    private Parameter getParameterOf(String name) {
        return parameters.stream()
                            .filter(p -> p.getName().equals(name))
                            .findFirst()
                            .orElse(null);
    }



    // =================================================== ~~~~~~~~~~~~~~~~~~~~~~~~
    // The liability of ParameterList is that we have to write a lot of boilerplate getter methods.
    // However, because most parameter to any strategy class is a primitive type (or String), we don't
    // have to continiously add new methods; this is thus acceptable.

    // === A getter for each type of {@code Parameter} is needed ~~~~~~~~~~~~~~~~~~~~~~~~
    public StringParameter getStringParameterOf(String name) {
        return (StringParameter) getParameterOf(name);
    }

    public IntegerParameter getIntegerParameterOf(String name) {
        return (IntegerParameter) getParameterOf(name);
    }

    // === A value of each type of {@code Parameter} is needed ~~~~~~~~~~~~~~~~~~~~~~~~
    public String getValueOfStringParameter(String name) {
        return ((StringParameter) getParameterOf(name)).getValue();
    }

    public int getValueOfIntegerParameter(String name) {
        return ((IntegerParameter) getParameterOf(name)).getValue();
    }

    // =================================================== ~~~~~~~~~~~~~~~~~~~~~~~~


    public ParameterList clone() throws CloneNotSupportedException {
        return (ParameterList) super.clone();
    }
    
}

GitHub: all code

于 2021-01-13T23:02:30.353 回答
3

I really liked 'SpaceTrucker's suggestion, that sometimes problems are solved by moving the abstraction to a different level :)

But if your original design makes more sense (which only you can tell, based on your feel of the spec) - then IMHO one can either: 1) Keep your approach of "loading everything into StrategyParameter" 2) Or move this responsibility to the Strategy

For option (2), I assume there's some common entity (account? customer?) from which one can deduce the department/country. Then you have "CountryClerkResolver.resolveClerk(String accountId)" which would look up the country.

IMHO both (1),(2) are legitimate, depending on context. Sometimes (1) works for me, because all params (department+country) are cheap to pre-load. Sometimes I even manage to replace the synthetic 'StrategyParameter' with a business-intuitive entity (e.g. Account). Sometimes (2) works better for me, e.g. if 'department' and 'country' required separate and expensive lookups. It becomes especially noticed with complex params - e.g. if a strategy selects clerks based on their scores in 'customer satisfaction' reviews, that's a complex structure which shouldn't be loaded for simpler strategies.

于 2013-11-14T11:03:01.050 回答
2

Let us start by assuming that your code is based on a simple if-else-if blocks.

In such a scenario, you will still need to have all the required inputs upfront. There is no getting around it.

By using the strategy pattern, you start decoupling your code - i.e., you define the base interface and concrete implementation.

Just having this design isn't good enough, because you still need to have an if-else-if block.

At this point, you can look at the following design changes:

  1. Use a factory pattern to load all the available strategies from this system. This could be based on meta information, like the Service Loader pattern that is available in the JDK.

  2. Identify a strategy by which you can query the available implementations to find out if they can handle the given input set of parameters. This can be as simple as canYouResolve(input) != null. By doing this we change from an if-else-if block to an for-each loop.

  3. In your case, you have a Default Implementation as well. So, let us say that the default implementation is part of your module and the other strategies are coming in from the other jars (that get loaded via the ServiceLoader from point 1).

  4. When your code kicks-in, you first look for all available strategies; ask them if they can handle the current scenario; if none of them can handle it, then use the default implementation.

If for some reason, you have more than one resolver being able to handle a particular input, you should consider defining a priority for those resolvers.

Now, coming to the input parameters, can these parameters be derived from some input object? If so, then why not send that input object itself to the resolver.

Note: This is very similar to how the JavaEE ELResolver works - In that case, the code marks the EL as resolved, thereby informing the root class that resolution is complete.

Note: If you think the service loader is too heavy, then look at searching for all META-INF/some-file-that-you-like to identify the resolvers that are available in the system.

From my own experience, most of the times, you end up writing code that mixes patterns to achieve the use case at hand.

Hope this helps your scenario.

于 2013-11-14T09:12:44.480 回答
-2

Since Java is statically typed, one good way of simulating dynamic objects is by using a Map. I would do this for passing dynamic parameters to my resolvers:

class StrategyParameter extends Map {} 
// Map could be used directly, but this make the code more readable

Then, my strategy pattern becomes: interface ClerkResolver { String resolveClerk(StrategyParameter strategyParameter); }

class DefaultClerkResolver implements ClerkResolver {

    public String resolveClerk(StrategyParameter strategyParameter) {
        // strategyParameter.get("department");
    }
}

class CountryClerkResolver implements ClerkResolver {

    public String resolveClerk(StrategyParameter strategyParameter) {
        // strategyParameter.get("country");
    }

}
于 2013-11-14T09:38:16.753 回答