The Strategy pattern is much like the State pattern in outline but a little different in intent. The Strategy pattern consists of a number of related algorithms encapsulated in a driver class called the Context. Your client program can select one of these differing algorithms, or in some cases, the Context might select the best one for you. The intent is to make these algorithms interchangeable and provide a way to choose the most appropriate one. The difference between State and Strategy is that the user generally chooses which of several strategies to apply and that only one strategy at a time is likely to be instantiated and active within the Context class. By contrast, as we have seen, it is possible that all of the different States will be active at once, and switching may occur frequently between them. In addition, Strategy encapsulates several algorithms that do more or less the same thing, whereas State encapsulates related classes that each do something somewhat differently. Finally, the concept of transition between different states is completely missing in the Strategy pattern.
A program that requires a particular service or function and that has several ways of carrying out that function is a candidate for the Strategy pattern. Programs choose between these algorithms based on computational efficiency or user choice. There can be any number of strategies, more can be added, and any of them can be changed at any time.
There are a number of cases in programs where we'd like to do the same thing in several different ways. Some of these are listed in the Smalltalk Companion.
Save files in different formats.
Compress files using different algorithms
Capture video data using different compression schemes.
Use different line-breaking strategies to display text data.
Plot the same data in different formats: line graph, bar chart, or pie chart.
In each case we could imagine the client program telling a driver module (Context) which of these strategies to use and then asking it to carry out the operation.
The idea behind Strategy is to encapsulate the various strategies in a single module and provide a simple interface to allow choice between these strategies. Each of them should have the same programming interface, although they need not all be members of the same class hierarchy. However, they do have to implement the same programming interface.
Let's consider a simplified graphing program that can present data as a line graph or a bar chart. We'll start with an abstract PlotStrategy class and derive the two plotting classes from it, as illustrated in Figure 30-1.
Our base PlotStrategy class acts as an interface containing the plot routine to be filled in in the derived strategy classes. It also contains the max and min computation code, which we will use in the derived classes by containing an instance of this class.
'Interface PlotStrategy Private xmin As Single, xmax As Single Private ymin As Single, ymax As Single Const max = 1E+38 Public Sub plot(x() As Single, y() As Single) 'to be filled in 'in implementing classes End Sub '----- Public Sub findBounds(x() As Single, y() As Single) Dim i As Integer xmin = max xmax = -max ymin = max ymax = -max For i = 1 To UBound(x()) If x(i) > xmax Then xmax = x(i) If x(i) < xmin Then xmin = x(i) If y(i) > ymax Then ymax = y(i) If y(i) < ymin Then ymin = y(i) Next i End Sub '----- Public Function getXmax() As Single getXmax = xmax End Function '----- Public Function getYmax() As Single getYmax = ymax End Function '----- Public Function getXmin() As Single getXmin = xmin End Function '----- Public Function getYmin() As Single getYmin = ymin End Function
The important part is that all of the derived classes must implement a method called plot with two float arrays as arguments. Each of these classes can do any kind of plot that is appropriate.
The Context class is the traffic cop that decides which strategy is to be called. The decision is usually based on a request from the client program, and all that the Context needs to do is to set a variable to refer to one concrete strategy or another.
'Class Context Dim fl As vbFile Dim x() As Single, y() As Single Dim plts As PlotStrategy '----- Public Sub setLinePlot() Set plts = New LinePlotStrategy End Sub '----- Public Sub setBarPlot() Set plts = New BarPlotStrategy End Sub '----- Public Sub plot() readFile plts.findBounds x(), y() plts.plot x(), y() 'do whatever kind of plot End Sub '----- Private Sub readFile() 'reads data in from data file End Sub
The Context class is also responsible for handling the data. Either it obtains the data from a file or database or it is passed in when the Context is created. Depending on the magnitude of the data, it can either be passed on to the plot strategies or the Context can pass an instance of itself into the plot strategies and provide a public method to fetch the data.
This simple program (Figure 30-2) is just a panel with two buttons that call the two plots. Each of the buttons is associated with a command object that sets the correct strategy and then calls the Context's plot routine. For example, here is the complete Line graph command class.
'Class LineCmd Implements Command Private contxt As Context '----- Public Sub init(cont As Context) Set contxt = cont End Sub '----- Private Sub Command_Execute() contxt.setLinePlot contxt.plot End Sub
The two strategy classes are pretty much the same: They set up the window size for plotting and call a plot method specific for that display panel. Here is the Line graph Strategy.
'Class LinePlotStrategy Implements PlotStrategy Dim plts As PlotStrategy Private Sub Class_Initialize() 'base class used to compute bounds Set plts = New PlotStrategy End Sub Private Sub PlotStrategy_findBounds(x() As Single, y() As Single) plts.findBounds x, y End Sub '----- 'not used in derived classes Private Function PlotStrategy_getXmax() As Single End Function Private Function PlotStrategy_getXmin() As Single End Function Private Function PlotStrategy_getYmax() As Single End Function Private Function PlotStrategy_getYmin() As Single End Function '----- Private Sub PlotStrategy_plot(x() As Single, y() As Single) Dim lplot As New LinePlot plts.findBounds x, y lplot.setBounds plts.getXmin, _ plts.getXmax, plts.getYmin, plts.getYmax lplot.Show lplot.plot x(), y() End Sub
Note that both the LinePlot and the BarPlot window have plot methods that are called by the plot methods of the LinePlotStrategy and BarPlotStrategy classes. Both plot windows have a setBounds method that computes the scaling between the window coordinates and the x-y coordinate scheme.
Public Sub setBounds(xmn As Single, xmx As Single, ymn As Single, _ ymx As Single) xmax = xmx xmin = xmn ymax = ymx ymin = ymn h = Pic.Height w = Pic.Width xfactor = 0.9 * w / (xmax - xmin) xpmin = 0.05 * w xpmax = w - xpmin yfactor = 0.9 * h / (ymax - ymin) ypmin = 0.05 * h ypmax = h - ypmin bounds = True End Sub
In VB6 you use the Line command to draw both the line and the bar plots. However, these plotting commands are immediate and do not refresh the screen if a window is obscured and needs to be redrawn. So we save the references to the x and y arrays and also call the plot method from the PictureBox's paint event.
Public Sub plot(xp() As Single, yp() As Single) Dim i As Integer, ix As Integer, iy As Integer 'draw a line plot x = xp y = yp ix = calcx(x(1)) iy = calcy(y(1)) Pic.Cls 'clear the picture Pic.PSet (ix, iy) 'start the drawing point 'draw the lines For i = 2 To UBound(x()) ix = calcx(x(i)) iy = calcy(y(i)) Pic.Line -(ix, iy), vbBlack Next i End Sub '------ Private Function calcx(ByVal xp As Single) As Integer Dim ix As Integer ix = (xp - xmin) * xfactor + xpmin calcx = ix End Function '------ Private Function calcy(ByVal yp As Single) As Integer Dim iy As Integer yp = (yp - ymin) * yfactor iy = ypmax - yp calcy = iy End Function '------ Private Sub Pic_Paint() plot x(), y() End Sub
The UML diagram showing these class relations is shown in Figure 30-3.
The final two plots are shown in Figure 30-4.
The class diagram is given in Figure 30-5.
The VB7 version of Strategy differs primarily in that we do not need to duplicate code between the two Strategies or the two windows, since we can use inheritance to make the same code work for both strategies. We define our basic PlotStrategy class as an empty class that must be overridden.
Public MustInherit Class PlotStrategy Public MustOverride Sub plot(ByVal x() As Single, _ ByVal y() As Single) End Class
The two instances for LinePlotStrategy and BarPlotStrategy differ only in the plot window they create. Here is the LinePlotStrategy.
Public Class LinePlotStrategy Inherits PlotStrategy Public Overrides Sub plot(ByVal x() As Single, _ ByVal y() As Single) Dim lplot As New LinePlot() lplot.Show() lplot.plot(x, y) End Sub End Class
And here is the BarPlotStrategy.
Public Class BarPlotStrategy Inherits PlotStrategy Public Overrides Sub plot(ByVal x() As Single, _ ByVal y() As Single) Dim bplot As New BarPlot() bplot.Show() bplot.plot(x, y) End Sub End Class
All of the scaling computations can then be housed in one of the plot window classes and inherited for the other. We chose the BarPlot window as the base class, but either one would work as well as the other as the base. This class contains the scaling routines and creates an array of SolidBrush objects for the various colors to be used in the bar plot.
Public Overridable Sub set_Bounds() findBounds() 'compute scaling factors h = Pic.Height w = Pic.Width xfactor = 0.8F * w / (xmax - xmin) xpmin = 0.05F * w xpmax = w - xpmin yfactor = 0.9F * h / (ymax - ymin) ypmin = 0.05F * h ypmax = h - ypmin 'create array of colors for bars colors = New arraylist() colors.Add(New SolidBrush(Color.Red)) colors.Add(New SolidBrush(color.Green)) colors.Add(New SolidBrush(color.Blue)) colors.Add(New SolidBrush(Color.Magenta)) colors.Add(New SolidBrush(color.Yellow)) End Sub
The plotting amounts to copying in a reference to the x and y arrays, calling the scaling routine and then causing the Picturebox control to be refreshed, which will then call the paint routine to paint the bars.
Public Sub plot(ByVal xp() As Single, _ ByVal yp() As Single) x = xp y = yp set_Bounds() 'compute scaling factors hasData = True pic.Refresh() End Sub '----- Public Overridable Sub Pic_Paint(_ ByVal sender As Object, _ ByVal e As PaintEventArgs) Handles Pic.Paint Dim g As Graphics = e.Graphics Dim i, ix, iy As Integer Dim br As Brush If hasData Then For i = 0 To x.Length - 1 ix = calcx(x(i)) iy = calcy(y(i)) br = CType(colors(i), brush) g.FillRectangle(br, ix, h - iy, 20, iy) Next End If End Sub
The LinePlot window is much simpler now because we can derive it from the BarPlot window and reuse nearly all the code.
Public Class LinePlot Inherits BarPlot Private bPen As Pen Public Sub New() MyBase.New LinePlot = Me InitializeComponent() bpen = New Pen(Color.Black) End Sub Public Overrides Sub Pic_Paint(ByVal sender As Object, _ ByVal e As PaintEventArgs) Handles Pic.Paint Dim g As Graphics = e.Graphics Dim i, ix, iy, ix1, iy1 As Integer Dim br As Brush If hasData Then For i = 1 To x.Length - 1 ix = calcx(x(i - 1)) iy = calcy(y(i - 1)) ix1 = calcx(x(i)) iy1 = calcy(y(i)) g.drawline(bpen, ix, iy, ix1, iy1) Next End If End Sub End Class
The two buttons are now command buttons whose Execute methods set the context and carry out the requisite plot. Here is the complete Line but ton code:
Public Class LineBtn Inherits System.Windows.Forms.Button Implements Command Private contxt As Context '----- Public Sub setContext(ByVal ctx As Context) contxt = ctx End Sub '----- Public Sub Execute() Implements vbnetStrategy.Command.Execute contxt.setLinePlot() contxt.plot() End Sub End Class
The two resulting plot windows are identical to those drawn in the VB6 version (Figure 30-4).
Strategy allows you to select one of several algorithms dynamically. These algorithms can be related in an inheritance hierarchy, or they can be unrelated as long as they implement a common interface. Since the Context switches between strategies at your request, you have more flexibility than if you simply called the desired derived class. This approach also avoids the sort of conditional statements that can make code hard to read and maintain.
On the other hand, strategies don't hide everything. The client code is usually aware that there are a number of alternative strategies, and it has some criteria for choosing among them. This shifts an algorithmic decision to the client programmer or the user.
Since there are a number of different parameters that you might pass to different algorithms, you have to develop a Context interface and strategy methods that are broad enough to allow for passing in parameters that are not used by that particular algorithm. For example the setPenColor method in our PlotStrategy is actually only used by the LineGraph strategy. It is ignored by the BarGraph strategy, since it sets up its own list of colors for the successive bars it draws.