Strategy Design Pattern in Delphi
This session consists of the development of a small application to read and pretty-print XML and CSV files. Along the way, we explain and demonstrate the use of the following patterns: State, Interpreter, Visitor, Strategy, Command, Memento, and Facade.
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
We need to store some information about the document we are looking at. In many patterns, the class that holds information used by the main participants in the pattern is called a context (we could have used one in Interpreter, remember?). In our case, the document class acting as the context, and is pretty simple:
TDocument = class(TObject) private FFileText : TStringList; FStrategy : TDocumentStrategy; function GetText : string; procedure SetText(const Value : string); function GetMemento : TDocumentMemento; procedure SetMemento(const Value : TDocumentMemento); protected public constructor Create; destructor Destroy; override; procedure OpenFile(const FileName : string); procedure CloseFile; procedure SearchAndReplace(const FindText,ReplaceText : string); procedure PrettyPrint; property Text : string read GetText write SetText; property Memento : TDocumentMemento read GetMemento write SetMemento; end;
OpenFile method uses the
LoadFromFile method of the stringlist field FFileText to load the text into its lines. The
CloseFile method clears the lines. The lines can be accessed as a string via the
Text property. The remaining property and methods will be discussed later when we see them being called. The code for our document class is in the Document.pas file.
The first thing we will look at is the
procedure TDocument.OpenFile(const FileName : string); begin FFileText.LoadFromFile(FileName); // Could use Factory Method here, but for now, just inline the code to // create the new strategy object FreeAndNil(FStrategy); if ExtractFileExt(FileName) = '.csv' then begin FStrategy := TCsvStrategy.Create(Self); end else if ExtractFileExt(FileName) = '.xml' then begin FStrategy := TXmlStrategy.Create(Self); end; end;
Here you can see that we load the specified file using the stringlist's file loading method. We then create a strategy object depending on the file extension. In our simple example we're only going to deal with CSV and XML files.
To understand why we've done this, we need to look at the Strategy pattern. This pattern allows us to define several different algorithms for the same thing, each one in a class of its own, and choose between them by using an object of the relevant class. In our case, we're interested in hiding the details of the search-and-replace and pretty printing from users our document.
procedure TDocument.SearchAndReplace(const FindText,ReplaceText : string); begin if Assigned(FStrategy) then begin FStrategy.SearchAndReplace(FindText,ReplaceText); end; end; procedure TDocument.PrettyPrint; begin if Assigned(FStrategy) then begin FStrategy.PrettyPrint; end; end;
As you can see, the implementation of the two methods is deferred to the strategy object. The base strategy class is defined as:
TDocumentStrategy = class(TObject) private FDocument : TDocument; protected property Document : TDocument read FDocument write FDocument; public constructor Create(ADocument : TDocument); virtual; procedure SearchAndReplace(const FindText,ReplaceText : string); virtual; abstract; procedure PrettyPrint; virtual; abstract; end;
This is an abstract class, because we want to force descendant classes to implement both the methods. It's quite common for strategy objects to need to access properties of the context, and indeed that's what we will need to do. To facilitate this, the constructor takes the document as a parameter. Note the use of the Self Encapsulate Field refactoring so descendant strategies can be declared in other units and still have access to the document, i.e. the document property is declared in the protected section.
In the file DocumentStrategy.pas you can see the implementations of the two strategy classes. A look at the two
SearchAndReplace methods should give you some idea why we needed to use this pattern:
procedure TCsvStrategy.SearchAndReplace(const FindText,ReplaceText : string); begin Document.Text := StringReplace(Document.Text,FindText,ReplaceText,[rfReplaceAll,rfIgnoreCase]); end; procedure TXmlStrategy.SearchAndReplace(const FindText,ReplaceText : string); begin FParser.Parse(Document.Text,FInterpreter.XmlDoc); FInterpreter.XmlDoc.SearchAndReplace(FindText,ReplaceText,True); // Pretty print as well, just so we can get some output FVisitor.Clear; FInterpreter.XmlDoc.Accept(FVisitor); Document.Text := FVisitor.Text; end;
As you can see, the two are quite different. While it would be no great hassle for the user of the document to call the Delphi
StringReplace procedure on a CSV file, the code for XML files is quite different. We can also use the same classes to hide the details of more than one algorithm, as we do for pretty printing.
The normal alternative to using Strategy is to subclass the context, in this case our document class. We could have a
TCSVDocument and a
TXMLDocument, for instance. But this mixes the implementation of the algorithms with the document, and can make the document classes difficult to maintain.
The class hierarchy can also be difficult to structure, particularly where there is more than one algorithm to consider. If one branch of the hierarchy needs to share an implementation from another branch, as well as one from its own, life gets a bit difficult.
You could also get the same behaviour by using case statements or lists of conditionals (
if..then..else if…) in the
TDocument class. In our trivial case, this might not appear too bad, but it quickly gets out of hand, and leads to the Switch Statements bad smell. See the Refactoring paper for why this is an undesirable state of affairs, and what to do to fix it.
There are some downsides, though. There are an increased number of objects in the system (but in my opinion, they will always be smaller and easier to maintain). The strategy and context can be closely coupled, as in our example. It is possible to remedy this by just passing the needed parameters in the strategy methods. I don't have a big problem with a certain amount of coupling, and in any case, some is unavoidable in this pattern.
In our example, the context (i.e. document) created the strategies as needed, but it is also common for the client to create the relevant one and pass it to the context. So we could get the user interface code to create the strategy, for instance. I have found that I can always refactor that so that the context can create the strategy it needs, but maybe there are situations this is not possible or desirable. In either case, you can see that something else needs knowledge of the strategies in order to choose between them.
Variations of the pattern exist.One of the more useful ones is making the context have a default implementation of the algorithm, and only creating a strategy object in certain situations. We could have the document use the
StringReplace procedure for all documents, for instance, only using something else in cases like XML files.
Note that this pattern is similar to the Template pattern, which encapsulates one algorithm in a class, and lets subclasses vary certain parts of that one algorithm. An example is a sorting algorithm, where the base class might implement a quicksort, and the subclasses can define the comparison function differently. We'll see another example in the Command pattern.