One of the dominant strategies of object-oriented design is the “open-closed principle”.
Figure demonstrates how this is routinely achieved - encapsulate interface details in a base class, and bury implementation details in derived classes. Clients can then couple themselves to an interface, and not have to experience the upheaval associated with change: no impact when the number of derived classes changes, and no impact when the implementation of a derived class changes.
A generic value of the software community for years has been, “maximize cohesion and minimize coupling”. The object-oriented design approach shown in figure is all about minimizing coupling. Since the client is coupled only to an abstraction (i.e. a useful fiction), and not a particular realization of that abstraction, the client could be said to be practicing “abstract coupling” . an object-oriented variant of the more generic exhortation “minimize coupling”.
A more popular characterization of this “abstract coupling” principle is “Program to an interface, not an implementation”.
Clients should prefer the “additional level of indirection” that an interface (or an abstract base class) affords. The interface captures the abstraction (i.e. the “useful fiction”) the client wants to exercise, and the implementations of that interface are effectively hidden.
The Interface entity could represent either an abstract base class, or the method signature expectations by the client. In the former case, the inheritance hierarchy represents dynamic polymorphism. In the latter case, the Interface entity represents template code in the client and the inheritance hierarchy represents static polymorphism.
A Strategy defines a set of algorithms that can be used interchangeably. Modes of transportation to an airport is an example of a Strategy. Several options exist such as driving one’s own car, taking a taxi, an airport shuttle, a city bus, or a limousine service. For some airports, subways and helicopters are also available as a mode of transportation to the airport. Any of these modes of transportation will get a traveler to the airport, and they can be used interchangeably. The traveler must chose the Strategy based on tradeoffs between cost, convenience, and time.
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:
1: TDocument = class(TObject)2: private3: FFileText : TStringList;4: FStrategy : TDocumentStrategy;5:6: function GetText : string;7: procedure SetText(const Value : string);8: function GetMemento : TDocumentMemento;
9: procedure SetMemento(const Value : TDocumentMemento);10: protected11: public12: constructor Create;
13: destructor Destroy; override;
14:15: procedure OpenFile(const FileName : string);16: procedure CloseFile;
17: procedure SearchAndReplace(const FindText,ReplaceText : string);18: procedure PrettyPrint;
19:20: property Text : string read GetText write SetText;
21: property Memento : TDocumentMemento read GetMemento write SetMemento;22: end;
23:
The 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 OpenFile
method:
1: procedure TDocument.OpenFile(const FileName : string);2: begin
3: FFileText.LoadFromFile(FileName);4:5: // Could use Factory Method here, but for now, just inline the code to
6: // create the new strategy object
7: FreeAndNil(FStrategy);8:9: if ExtractFileExt(FileName) = '.csv' then begin10: FStrategy := TCsvStrategy.Create(Self);11: end else if ExtractFileExt(FileName) = '.xml' then begin12: FStrategy := TXmlStrategy.Create(Self);13: end;
14: 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.
1:2: procedure TDocument.SearchAndReplace(const FindText,ReplaceText : string);3: begin
4: if Assigned(FStrategy) then begin5: FStrategy.SearchAndReplace(FindText,ReplaceText);6: end;
7: end;
8:9: procedure TDocument.PrettyPrint;
10: begin
11: if Assigned(FStrategy) then begin12: FStrategy.PrettyPrint;13: end;
14: end;
15:
As you can see, the implementation of the two methods is deferred to the strategy object. The base strategy class is defined as:
1: TDocumentStrategy = class(TObject)2: private3: FDocument : TDocument;4: protected5: property Document : TDocument read FDocument write FDocument;6: public7: constructor Create(ADocument : TDocument); virtual;8:9: procedure SearchAndReplace(const FindText,ReplaceText : string); virtual; abstract;10: procedure PrettyPrint; virtual; abstract;11: end;
12:
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 descendent 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:
1: procedure TCsvStrategy.SearchAndReplace(const FindText,ReplaceText : string);2: begin
3: Document.Text := StringReplace(Document.Text,FindText,ReplaceText,[rfReplaceAll,rfIgnoreCase]);4: end;
5:6: procedure TXmlStrategy.SearchAndReplace(const FindText,ReplaceText : string);7: begin
8: FParser.Parse(Document.Text,FInterpreter.XmlDoc);9: FInterpreter.XmlDoc.SearchAndReplace(FindText,ReplaceText,True);10: // Pretty print as well, just so we can get some output
11: FVisitor.Clear;12: FInterpreter.XmlDoc.Accept(FVisitor);13: Document.Text := FVisitor.Text;14: end;
15:
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.