Last time, we created the IntegerSequence and FibonacciSequence classes that both implemented IEnumerable<T>. We saw how we could implement these with separate classes that implement IEnumerator<T> (IntegerEnumerator and FibonacciEnumerator) or with the "yield return" statement.
This time, we'll see how we can use the Strategy pattern to create a single IEnumerable<T> to which we can pass a specific sequence algorithm. This will allow us to easily add other sequence algorithms in the future.
The sample code can be downloaded from here: JeremyBytes - Downloads.
The Strategy Pattern
The Strategy pattern is one of the Gang of Four design patterns. Here is the GoF description of the Strategy pattern:
"Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it." [Gamma, Helm, Johnson, and Vlissides. Design Patterns. Addison-Wesley, 1995.]This means that rather than having the algorithm hard-coded with the class, it is passed in (often as a constructor parameter). This allows us to create different algorithms that are all interchangeable and can be used with the same class.
Benefits of Strategy
One of the benefits of the Strategy pattern is that it passes control of which algorithm will be used from the class to the client using the class. This means that the client is responsible for picking which strategy to use and then passing it to the class that will use it. We'll see how this works in just a bit.
The Strategy pattern also allows us to create multiple implementations of the same behavior. This means that we can have different algorithms that ultimately perform the same function but have different priorities -- perhaps optimizing for speed or memory usage, depending on what is important to the client. For example, a mobile application may want an algorithm designed to minimize network usage while an intranet application wants to maximize speed without a concern for usage of the internal high-speed network.
Consequences of Strategy
As we've discussed previously (Design Patterns: Understand Your Tools), design patterns have both pros and cons. One of the consequences of the Strategy pattern is that the client must be aware of the different strategies that are available. Often in non-strategy implementations, the client passes a string or enum to select an algorithm, but it has no direct knowledge of the algorithms themselves. When using the Strategy pattern, the client is responsible for instantiating the strategy class, and so it must be aware of what classes are available.
Another consequence is that we generally end up with an increased number of classes. As always, we need to weigh the benefits and consequences before we decide to implement a specific pattern.
Strategy and the Sequence Classes
What we saw in Part 3 is that the IntegerSequence and FibonacciSequence classes varied only in terms of the specific IEnumerator<T> concrete type that they "new" up. This makes it a good candidate for us to create a more generic class and then pass in that concrete type.
The client will need to be aware of the various concrete types. This is okay in our case because the client was already responsible for choosing the specific IEnumerable<T> concrete type. With regard to having more objects, we are willing to accept this since we'll have isolated and easily-interchangeable classes.
As a reminder, the Strategy Pattern is a design pattern; it does not specify a particular implementation. The implementation we use here is just one way to implement the pattern. There are countless others.
The StrategicSequence Class
Let's do some planning before we implement the StrategicSequence class. Let's start by looking at the IntegerSequence class from the last article:
To make this conducive to the Strategy pattern, we'll do a couple of things. First, we will create a private variable for the strategy (which will mirror the IntegerEnumerator in this case). Then we will update our constructor to accept that strategy as a parameter.
Things get a little more complicated here: we need to figure out how to get the "NumberOfValues" value into the strategy. We could do this with the strategy constructor (like we have above in the IntegerEnumerator constructor), but this may not be the best choice. What we will do instead is create a "NumberOfValues" property in the strategy that we can use to set this value.
Since each of our strategy classes needs to have a property for "NumberOfValues", it makes sense for us to create an interface that includes this property. In the sample solution, the SequenceLibrary project contains a folder called StrategicEnumerators. We'll add a new class under this folder: right-click the folder, select "Add", then "Class", then type "IStrategicEnumerator.cs" in the dialog.
Then we'll update the file as follows:
Notice that IStrategicEnumerator is an interface that also specifies the IEnumerator<int> interface. This is so that we will get all of the properties and methods contained in IEnumerator<T> in addition to the NumberOfValues property.
Next, we'll add our first strategy class that will implement the IStrategicEnumerator interface. This will be "IntegerStrategy" that will mirror the functionality of the IntegerEnumerator class that we implemented last time. (Note: if this were a "real life" application, we would probably just re-purpose the IntegerEnumerator class. But in this case, we'll leave the original in tact so we can refer to the code if we want.)
Add the "IntegerStrategy" class under the StrategicEnumerators folder, add "using System.Collections", and specify the IStrategicEnumerator interface:
Just like last time, we'll right-click on "IStrategicEnumerator" and select "Implement Interface" to let Visual Studio stub out the properties and methods for us. Note that the NumberOfValues property is implemented as well as the properties and methods of IEnumerator<T>.
After a little re-ordering, and converting "NumberOfValues" to an automatic property, we end up with the following:
For our implementation, we can copy from the IntegerEnumerator class that we created previously. Here are the fields and properties (including "_position"):
And here are the methods:
Other than "NumberOfValues" being a property rather than a field, all of the other code is the same.
Now that we have our first strategy class, let's create the class that can use it. The StrategicSequence class will allow us to pass in whichever concrete strategy that we want.
Follow these steps:
- Add the new "StrategicSequence" class to the root of the project.
- Add "using System.Collections".
- Add "using SequenceLibrary.StrategicEnumerators" (where IStrategicEnumerator is located).
- Make the class public.
- Specify that StrategicSequence implements "IEnumerable<int>".
- Implement the IEnumerable<T> interface.
That gets us a good start. Let's look at the completed code:
Let's compare this to the original IntegerSequence. First, we have a new private field to hold the strategy. Next, we have the NumberOfValues property (just like before). Then, the constructor takes both a strategy and the number of values, and it updates the appropriate class values.
GetEnumerator() is a bit different. Here, we set the NumberOfValues property of the strategy based on the NumberOfValues property of our StrategicSequence class. Then we return the strategy -- this is valid since the strategy implements the IEnumerator<int> interface.
This gives us a working sequence class that implements the Strategy pattern.
Using the StrategicSequence Class
Now let's flip over to the console application to see the output of our class. We'll need to add a using statement for "SequenceLibrary.StrategicEnumerators" (since that is where our strategy class is).
Here's our updated code:
First, we create an instance of our strategy by calling "new IntegerStrategy()". Then we create a new StrategicSequence and pass in our strategy and the number of values. The rest of our code is exactly the same as before.
And we get the expected output:
But the advantage of using the strategy pattern is that we can create multiple algorithms that are all interchangeable.
Now that we get the idea, we can create additional strategies. We won't go through all of the code details here since the implementations will mirror what we did with the IntegerStrategy. You can check the download for the completed code. The "Starter" solution contains these strategies in the "StrategicEnumerators" folder. You can add them to the SequenceLibrary project, by right-clicking on the StrategicEnumerators folder, select "Add", then "Existing Item" and then locate the 2 files: FibonacciStrategy.cs and SquareStrategy.cs.
FibonacciStrategy contains the algorithm for the Fibonacci Sequence. Again, this is very similar to the FibonacciEnumerator class that we implemented previously (with a few updates such as changing the _numberOfValues field to the NumberOfValues property).
SquareStrategy contains an algorithm that returns a series of squares (by returning "_position * _position").
The FibonacciStrategy and SquareStrategy classes contain a check ensure that we do not overflow Int32. As we saw last time, this occurs after 47 values in the Fibonacci Sequence. For the sequence of squares, this occurs after about 50,000 values. We don't have to worry about this error check for the IntegerStrategy since there is a one-to-one relationship between the returned integer value and the total number of values returned (meaning, we can't overflow the return value without overflowing the NumberOfValues first).
As a side note, the reason that we are using "int" for the Fibonacci Sequence instead of a "long" is so that we will have the same generic type ("int") across all of our strategies. We could change this to a "long" for all of our classes if we wish; I left it as "int" for simplicity.
Using the FibonacciStrategy
In order to use the FibonacciStrategy in our console app, we only need change the concrete type for our IStrategicEnumerator:
Which gives us the following output:
It is just as easy to switch this over to the "SquareStrategy".
Benefits of the StrategicSequence
We get a few different benefits from implementing the Strategy pattern in our StrategicSequence class. First, our sequence class has been made extensible by abstracting out the algorithm (the "strategy"). This means that we can easily create new strategies for other sequences -- such as a a prime number strategy or even a random number strategy.
Next, the client (the console application in our case) gets to decide which strategy will be used. Even though our console application is making this decision at compile time, we can easily move this decision to run time. Picture the following application:
In this case, we have a set of radio buttons that let us pick which algorithm we want to use to generate our sequence. Based on this selection, the client will "new" up the appropriate strategy and pass it to the StrategicSequence class. (Note: this application is not included in the code download; it is left as an exercise for the reader.)
Although the Strategy pattern is designed around having multiple strategies available for the client to select from, we can also think about how our code would work when using an Inversion of Control (IoC) container. In this scenario, the container would be responsible for instantiating the appropriate strategy based on configuration (similar to how we handled dynamically loading a repository in the final sample of IEnumerable, ISaveable, IDontGetIt: Interfaces in .NET). Then the strategy will be passed to the sequence class.
Things to Think About
There are a couple of things to think about regarding our implementation of the Strategy pattern. The biggest concern is that the NumberOfValues property in the strategy is a publicly exposed property. This means that we can alter this value directly in our client application -- including while the sequence is being generated.
Consider the following:
This results in the sequence returning 5 values instead of the originally-specified 12.
Another option would be to make NumberOfValues a constructor parameter (similar to our original FibonacciEnumerator class). In this scenario, NumberOfValues could be a private field of the strategy class rather than a publicly exposed property. It would be used as follows:
The advantage to this implementation is that we can no longer modify the NumberOfValues while the sequence is being iterated. But there are a couple of disadvantages. First, the usage is not as intuitive -- instead of passing the NumberOfValues to the StrategicSequence constructor, we pass it to the strategy object constructor. This strikes me as being the wrong place for this value; it makes more sense to tell the sequence how many values you want rather than telling the strategy.
Another disadvantage is that you cannot put a constructor into an interface. This means that by using interfaces, we cannot enforce that each strategy class would include a constructor with 1 parameter. This would become a convention rather than something that would cause a compiler error.
Because of these disadvantages, I decided to go with the first option (the publicly exposed property). I am willing to risk the possibility that someone would modify the sequence length based on the other options. I'm sure that there are other options as well. Be sure to drop me a note if you have a better implementation.
Today, we took a look at the Strategy pattern and how we can use it to create a StrategicSequence. This shifts the control of what type of sequence is generated to the client. We also have an easy way to plug in new sequence algorithms. Since StrategicSequence implements IEnumerable<T>, we still get all of the advantages of that interface.
Next time, we'll review the IEnumerable<T> interface, the advantages that the interface provides, and what we've done with various implementations of the interface. Until then...
Post a Comment