Reputation: 1075
I am currently learning design patterns on my own. As I studied the Strategy pattern I found something that looks strange for me. I looked for discussions on this pattern but none answered my question... which is how can I implement the Strategy pattern to let it be clean, to maintain encapsulation and to make adding a new strategy easy. To explain my problem here is the "canonical" strategy pattern:
public interface Strategy {
public void run();
}
public class stratConcrt1 implements Strategy {/*run() implementation*/}
public class stratConcrt2 implements Strategy {/*run() implementation*/}
public class Context {
private Strategy strategy;
public Context(Strategy strat) {
this.strategy = strat;
}
public void runStrategy() {
this.strategy.run()
}
}
public class Client {
public void main(Strings[] args) {
Context cx;
cx = new Context(new stratConcrt1())
cx.runStrategy();
cx = new Context(new stratConcrt2())
cx.runStrategy();
}
}
I understand what is going on, but I feel strange to let the client knows something about the different strategies that can be applied. For me it would be cleaner to let Context instantiate the different strategies and not Client, since Context deals with strategies it should (at least in my mind) be the only one to be able to instantiate the strategy.
I implemented a little example using JavaFx with some differences with the code above:
I have a class Field that instantiates a list of coordinates, this class has a method to sort the list. The method that sorts the list of coordinates is the method for which there are several strategies.
public class Field {
// a field contains rectangles described in a list through their coordinates
private ObservableList<Coordinate> mpv_coordinateList = FXCollections
.observableArrayList();
private Context mpv_sortContext;
// Constructor
public Field() {
//the rectangles are randomly created
Random rd = new Random();
for (int i = 0; i < 100; i++) {
mpv_coordinateList.add(new Coordinate(rd.nextInt(490), rd.nextInt(490)));
}
//a context to dynamically modify the sort algorithm
mpv_sortContext = new Context();
}
//returns the list with all rectangle
public ObservableList<Coordinate> getField() {
return this.mpv_coordinateList;
}
//sort elements (depending on different algorithms)
public ObservableList<Coordinate> sortElements(String p_sortToApply) {
return mpv_sortContext.launchSort(p_sortToApply,
this.mpv_coordinateList);
}
}
As the model says I created an interface and let the concrete strategies inherited from this interface:
public interface SortStrategy {
ObservableList<Coordinate> sort(ObservableList<Coordinate> p_listToSort);
}
public class EvenSort implements SortStrategy {
@Override
public ObservableList<Coordinate> sort(
ObservableList<Coordinate> p_listToSort) {
ObservableList<Coordinate> oddCoordList = FXCollections
.observableArrayList();
for (Coordinate coord : p_listToSort) {
if (coord.x % 2 == 0) {
oddCoordList.add(coord);
}
}
return oddCoordList;
}
}
public class OddSort implements SortStrategy {
@Override
public ObservableList<Coordinate> sort(
ObservableList<Coordinate> p_listToSort) {
ObservableList<Coordinate> oddCoordList = FXCollections
.observableArrayList();
for (Coordinate coord : p_listToSort) {
if (coord.x % 2 == 1) {
oddCoordList.add(coord);
}
}
return oddCoordList;
}
}
The concrete classes just return a list that contains all the coordinates that have even or odd x coordinate.
and then I created a class context:
public class Context {
//private SortStrategy mpv_sortStrategy; //never used
private EvenSort mpv_evenSort = new EvenSort();
private OddSort mpv_oddSort = new OddSort();
private StandardSort mpv_standardSort = new StandardSort();
private HashMap<String, SortStrategy> mpv_HashMapStrategies;
public Context() {
//creation of a dictionary with all possible strategies
mpv_HashMapStrategies = new HashMap<String, SortStrategy>();
mpv_HashMapStrategies.put("Even Sort", mpv_evenSort);
mpv_HashMapStrategies.put("Odd Sort", mpv_oddSort);
mpv_HashMapStrategies.put("Standard Sort", mpv_standardSort);
}
public ObservableList<Coordinate> launchSort(String p_sortToApply, ObservableList<Coordinate> p_listToSort){
return mpv_HashMapStrategies.get(p_sortToApply).sort(p_listToSort);
}
}
Through the gui a user can choose the strategy that he wants to use to sort the list. The user can click on a button to launch a sort. A call is done through the main class (not shown) to inst_field.mpv_sortContext.sortElements(a_string)
with a string as parameters that describes the strategy to use. This string is then used in sortElements to select the instance of the strategy that the user wants to apply.
With my implementation I have on one side the client and on the other side all the code that deals with Strategies (Context, interface and concrete classes). If I want to add a new strategy I just have to add an instanciation of the new strategy in the Context class and a description of this new strategy in the gui to let the user knows about it.
I am aware that in the implementation I done is also not so nice because Context contains an instance for each possible strategy and because of that I don't need a reference to the interface, but I find it cleaner than letting the Field and the client knows about these classes.
Well... Am I totally wrong? Is there something I missed in the "canonical" strategy pattern. Is the "canonical" way the one and only way to implement the Strategy pattern? or Is there a better way to implement this pattern in a way that only the classes which should know are aware of the strategy instances and in a way that adding a new strategy can be easily done?
Upvotes: 5
Views: 3834
Reputation: 7799
Your first implementation completely complies with the pattern definition (as per the classic Design Patterns - Elements of Reusable Object-Oriented Software), and IMHO you are attributing to it objectives that it simply never had. The key point you could be missing is that Context
contains (or is able to obtain from the environment) data that is passed to the encapsulated ConcreteStrategy
s without collaboration or knowledge of the client. In other words, the client has knowledge of what Strategy
it wishes to apply, but not of the data in the Context
. This for simple separation of concerns / decoupling.
Adapting these ideas to your first example, it could have read:
public interface Strategy {
public void runOn(Context context);
}
public class ConcreteStrat1 implements Strategy {
public void runOn(Context context) { ... }
}
public class ConcreteStrat2 implements Strategy {
public void runOn(Context context) { ... }
}
public class Context {
private Strategy strategy;
private InformationPiece1 ip1;
private InformationPiece2 ip2;
private InformationPiece3 ip3;
...
// These are the "ContextInterface()" methods: ways for the Strategy's and other clients to interact with the Context
public InformationPiece1 getIP1() { return this.ip1 ; }
public void setIP1(InformationPiece1 ip1) { this.ip1= ip1; }
public InformationPiece2 getIP2() { return this.ip2 ; }
public void setIP2(InformationPiece2 ip2) { this.ip2= ip2; }
public InformationPiece3 getIP3() { return this.ip3 ; }
public void setIP3(InformationPiece3 ip3) { this.ip3= ip3; }
...
public Context(Strategy strategy){
this.strategy= strategy ;
}
// This operation can be carried out according to a configurable Strategy
public void doSomething() {
this.strategy.runOn( this );
}
// This other doesn't. Or maybe it does, but with a second category of configurable Strategy's
public void doAnotherThing() {
...
}
}
public class Client {
public void main(Strings[] args) {
Context cx;
// Decide with what Strategy to "configure" cx.
if( args[0].equalsIgnoreCase("A") )
cx= new Context( new ConcreteStrat2() );
else
cx= new Context( new ConcreteStrat1() );
// Populate cx.
new CIBuilder(cx).buildFrom("Maybe a file name? User interaction anyone?") ;
// Pass cx to another client, which would eventually call cx.doSomething().
// This client doesn't need to know what Strategy will be called in turn by cx.doSomething().
// In fact, it doesn't need to know that cx.doSomething() is implemented using the Strategy Pattern at all!
new DoesntKnowAboutStrategiesNorNeedsTo().process(cx) ;
}
}
In this way, we have clearly separated roles and responsabilities:
main()
selects a Strategy
based on user input (or maybe later based on a property file; it will be very easy to change). It knows about the different available Strategy
's and that Context
accepts one on construction, but not much else about either. From this point of view, main()
is a client of the Strategy
pattern, but not much of Context
itself.Context
is a classic class able to store information and perform operations. However, one of these operations can be performed in several different ways (Strategy
s). Apart from this, Context
doesn't know how many different strategies exist or how they operate. In fact, if a new Strategy
appeared, Context
doesn't need to be modified at all (if the original design of its interfaces is good, though).DoesntKnowAboutStrategiesNorNeedsTo
is completely isolated from Strategy
s, as well as the process of selecting one. It only knows about Context
's interfaces, not their implementation. Even if doSomething()
had been originally implemented without the Strategy
pattern, DoesntKnowAboutStrategiesNorNeedsTo
would have not required modification after the change. This is the main client of Context
, but not of the Strategy
pattern at all.Upvotes: 0
Reputation: 7799
It looks to me that you are looking for a design pattern in which the main concern is selection of a Strategy
as opposed to having objects that can be configured with one. If this is the this case, the most adequated pattern is Chain of Responsibility
. It is similar to your second example. However, it's much more modular and extensible given that each Handler
(the equivalent in this pattern to Strategy
) is given the opportunity to decide (by itself) if it can be applied to the data or not.
Now, if the objective is deciding the best Strategy
(as opposed to the first that can operate on the given data), a relatively simple variant that I have used (and am sure many did before me) is to define a cost criterion. Execution time, maximum memory to be used, or a combination of both are good examples. Each Handler
/Strategy
needs to know how to estimate this cost for given data quickly. After defining this basic concept, there are two main implementation alternatives:
Define a central controller similar to the Context
in the second example with a catalog of Handler
/Strategy
s. Maybe predefined or maybe configurable in some way. This controller has every Handler
evaluating the data and producing a cost estimate, and calls the one with the minimum to do the real processing. This really deviates too much from the spirit of Chain of Responsability
to consider it a variant, but wanted to point it out because of its similarity with your original code. Another variant is to make the controller estimate the cost for each Handler
, but in this case concerns are separated in the wrong way and coupling between components results enormous.
Make Handler
s collaborate in finding not the first one that can process the Request
(data), but in finding the best according to the cost estimate.
Below is the general code for alternative 2, which presents the multiple benefits of Chain of Responsibility
(as well as its relatively few cons).
public abstract class Handler {
private Handler successor ;
public void setSuccessor(Handler successor) { this.successor= successor ; }
public abstract double estimateCostFor(Information info) ;
public abstract void doProcess(Information info) ;
public boolean process(Information info) {
return this.processIfBetterThan(Double.MAX_VALUE,info);
}
public boolean processIfBetterThan(double callersCost,Information info) {
double myCost= this.estimateCostFor(info) ;
double minCostSoFar= Math.min(callersCost,myCost) ;
boolean informationProcessed= false ;
if( this.successor != null )
informationProcessed= this.successor.processIfBetterThan(minCostSoFar,info) ;
if( ! informationProcessed && myCost <= minCostSoFar ) {
// In cases like this, I prefer <= to == especially when dealing with floating point variables.
// Much safer!
this.doProcess(info);
informationProcessed= true ;
}
return informationProcessed ;
}
}
public class ConcreteHandler1 implements Handler {
public double estimateCostFor(Information info) { ... }
public void doProcess(Information info) { ... }
}
public class ConcreteHandler2 implements Handler {
public double estimateCostFor(Information info) { ... }
public void doProcess(Information info) { ... }
}
public class ConcreteHandler3 implements Handler {
public double estimateCostFor(Information info) { ... }
public void doProcess(Information info) { ... }
}
public class Client {
public void main(Strings[] args) {
// Setup chain of responsibility
Handler startOfChain= new ConcreteHandler1() ;
Handler h2= new ConcreteHandler2() ;
startOfChain.setSuccessor( h2 );
Handler h3= new ConcreteHandler3() ;
h2.setSuccessor( h3 );
// Obtain Information to process
Information myInfo= ... ;
// Process it with the best Handler/Strategy
startOfChain.process(info);
}
}
Upvotes: 0
Reputation: 6298
I looked for discussions on this pattern but none answered my question... which is how can I implement the Strategy pattern to let it be clean
Your "strategy" isn't necessarily unclean, as you describe it and I think you may be getting bogged down with the idea of who the client is. Your client is providing the implementation to use but that may be a necessary implementation detail. For instance, the java RMI tutorial's ComputeEngine essentially uses just this pattern. The "compute" implementation is passed by the client - as only the client knows the computation to execute.
However, more commonly, the strategy is used to provide a way for logic to be made configurable within some context or to allow a common context to be tailored to specific uses. It also has the benefits of hiding internal structures from clients as required. Often to do this, the strategy to use will be configured internally to the context. This may be provided by:
Class.getResourceAsStream
). This is an extension to your Context
class's map (i.e. load the map from a well-known location). An example here is that you may provide a "strategy representing a default implementation to use but allow new implementations to be provided as an alternative strategy - e.g. a defaultXMLParserFor the first two points above, you could consider using a factory to derive the correct strategy. This would keep the implementation selection concern localised.
Well... Am I totally wrong? Is there something I missed in the "canonical" strategy pattern. Is the "canonical" way the one and only way to implement the Strategy pattern? or Is there a better way to implement this pattern in a way that only the classes which should know are aware of the strategy instances and in a way that adding a new strategy can be easily done?
I wold say that you are not wrong. It really depends on the purpose behind the use of the strategy. If it's an internal system concern then some rules should drive the selection (behind a factory). If it is configurable for whatever reason then it should be driven by configuration and the management hidden inside the context (class that manages the overall logic that uses the strategy). However, if it's dependent on user data or behaviour then either the data drives the selection internally or you have to accept that the client will have to pass you your strategy.
Note also that a goal behind this pattern is to remove conditional logic while keeping alternative implementations. So, if your strategy causes you to do lots of conditional logic then you may need to rethink if it clarifies your code.
</warandpeace>
Upvotes: 3
Reputation: 4569
You hold all the strategies in heap - it is not good. Firstly, strategy pattern often provides functionality for a long term, or even for all time app is running. So you don't need any another strategy except chosen. So, in case if you have a very big number of very big strategies, you'll hold in heap a lot of objects, that you don't need.
Also don't forget, that you can init your strategy with different parameters, in your case you have frozen objects and you can't modify them.
But don't look on each patter as axiom. You can modify and use it how do you want and how do you need. Patterns are main models, good practices etc, but each one can't be perfect for all solutions.
Upvotes: 2