Strategy Pattern with different parameters
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.
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.
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:
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
Algorithm
orParameter
. - 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 theparameters
array. (e.g. what ifparameters[0]
wasn'tcountry
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:
- To increase readability, an abstraction for the
-
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