29.5 Strategy
The strategy pattern applies when you have a general problem to be solved by two or more algorithms. The choice of solutions represents a decision the user makes. For example, a graphics program allows for saving an image in many different formats, each with unique code for writing a file. The input to each of these routines is identical.
This pattern can also solve the problem of presenting a Web application in various languages or styles. Very simple schemes can get by with an array of translated words or colors for a theme, but complex customization may require code to produce dynamic results. I encountered this situation when trying to allow for international versions of an e-commerce site.
Aside from differences in language, people of the world format numbers differently. The number_format function goes a long way to solve this problem, of course. It doesn't address figures of money. Americans use $ to the left of numbers to represent dollars. Europeans may expect EUR, the symbol for a Euro. It's possible prices for Japanese customers should have yen to the right of the figure, depending on the situation.
To implement the strategy pattern, you must define a shared interface for all algorithms. You may then proceed with various implementations of this interface. In PHP we can implement this by defining a general class and extending it with subclasses. We can take advantage of polymorphism to promote a consistent interface to the functionality.
Listing 29.4 contains the base class, localization. It defines two methods, formatMoney and translate. The first method returns a formatted version of a money figure. The second method attempts to translate an English phrase into a local representation. The base class defines default functionality. Subclasses can choose to use the defaults or override them.
Listing 29.4 Strategy pattern
<?php //Strategy superclass class Localization { function formatMoney($sum) { number_format($sum); } function translate($phrase) { return($phrase); } } ?>
Listing 29.5 contains an English subclass of localization. This class takes special care to place negative signs to the left of dollar signs. It doesn't override the translate method, since input phrases are assumed to be in English.
Listing 29.5 English subclass
<?php //get Localization include_once('29-4.php'); class English extends Localization { function formatMoney($sum) { $text = ""; //negative signs precede dollar signs if($sum < 0) { $text .= "-"; $sum = aba($sum); } $text .= "$" . number_format($sum, 2, '.', ','); return($text); } } ?>
Listing 29.6 contains a German subclass of localization. This class uses periods to separate thousands and commas to separate decimals. It also includes a crude translate method that handles only yes and no. In a realistic context, the method would use some sort of database or external interface to acquire translations.
Listing 29.6 German subclass
<?php include_once('29-4.php'); class German extends Localization { public function formatMoney($sum) { $text = "EUR " . number_format($sum, 2, ',', '.'); return($text); } public function translate($phrase) { if($phrase == 'yes') { return('ja'); } if($phrase == 'no') { return('nein'); } return($phrase); } } ?>
Finally, Listing 29.7 is an example of using the localization subclasses. A script can choose between available subclasses based on a user's stated preference or some other clue, such as HTTP headers or domain name. This implementation depends on classes kept in files of the same name. After initialization, all use of the localization object remains the same for any language.
Listing 29.7 Using localization
<?php print("Trying English<br>\n"); include_once('29-5.php'); $local = new English; print($local->formatMoney(12345678) . "<br>\n"); print($local->translate('yes') . "<br>\n"); print("Trying German<br>\n"); include_once('29-6.php'); $local = new German; print($local->formatMoney(12345678) . "<br>\n"); print($local->translate('yes') . "<br>\n"); ?>
One advantage of this pattern is the elimination of big conditionals. Imagine a single script containing all the functionality for formatting numbers in every language. It would require a switch statement or an if-else tree. It also requires parsing more code than you would possibly need for any particular page load.
Also consider how this pattern sets up a nice interface that allows later extension. You can start with just one localization module, but native speakers of other languages can contribute new modules easily. This applies to more than just localization. It can apply to any context that allows for multiple algorithms for a given problem.
Keep in mind that Strategy is meant for alternate functionality, not just alternate data. That is, if the only difference between strategies can be expressed as values, the pattern may not apply to the particular problem. In practice, the example given earlier would contain much more functionality differences between languages, differences which might overwhelm this chapter.
You will find the Strategy pattern applied in PEAR_Error, the error-handling class included in PEAR. Sterling Hughes wrote PEAR's error framework so that it uses a reasonable set of default behaviors, while allowing for overloading for alternate functionality depending on context.