CSC/ECE 517 Fall 2007/wiki1b 8 ktrk
The Question
Take a case of the Strategy pattern and implement it as succinctly as possible in Ruby and Java. Compare the two implementations in terms of clarity and succinctness. The example should be a "real-world" example. While it may be grossly oversimplified for the purpose of illustration, it should not be totally contrived (i.e., should not raise the question, Why would anyone ever want to do that?).
Introduction
What is the Strategy Pattern?
The strategy pattern is a proven object oriented design pattern that can be used to solve a common coupling problem often found in software development. Often during the development of compositional classes, behaviors and algorithms can become mixed into the class during development without realizing that these behaviors or algorithms may change, become more complex, or additional behaviors may need to be added. These changes could be implemented by extending the class's behavior through inheritance, but over time this could result in an overly complex class hierarchy. The strategy pattern is a design template that can be used to decouple the behavior and algorithms from the object being acted upon.
Generic Strategy Class Diagram
When to Use the Strategy Pattern
In the seminal book on design patterns by the Gang-of-Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides), situations (also labeled "code smells" by Martin Fowler in Refactoring) are described where the strategy design pattern could be used to architect a better solution. These include:
- A class that exhibits many similar behaviors or uses different variations of the same algorithm. Instead of using conditional statements within the class to select the behavior or algorithm, use the strategy pattern to take advantage of polymorphism.
- A class that uses data structures related to a behavior of the class but not the class itself. The strategy pattern can encapsulate the data specific to the behavior.
- Many similar classes that differ only in a type of behavior. These classes can be combined into a single class with the behavior refactored into separate classes so that the behavior can be selected by the instantiated class.
Advantages of the Strategy Pattern
By decoupling through the use of composition rather than inheritance, several advantages emerge that improve the maintainability and readability of the code.
- Allows classes to change behavior at runtime or design time
- Decreases code duplication among classes using variations of the same behavior [1]
- Behavior is better encapsulated by not being buried in its context [2]
- A well known design pattern communicates the intent of the code more readily
Similar Patterns
Use the state pattern when the state of the object changes with a change in behavior.
Use the bridge pattern when a structural design is needed.
Use the command pattern when the invoking object (the context object in the state pattern) does not have knowlege of the recipient object (the behavior in the state pattern). The intermediary command object takes care of dispatching the request to the appropriate object.
For a discussion of the strategy pattern compared to the state and bridge patterns see Strategy Pattern vs. Bridge Pattern.
Strategy Pattern Implementation in Java and Ruby
For our example, we chose to model a video game where there could be different types of Fighters pitted against each other, as you might see in the UFC. In our case, the strategy is the Attack. Each Fighter has a different type of Attack according to his training. The Fighter is therefore an "abstract" class. In statically-typed Java, the class itself is declared as abstract, and in dynamic Ruby it must be extended (or else the Fighter will not know any techniques). To keep things simple, our fighters are not very smart or well-trained, so they each only get one Attack. To see our Fighters in action, there is also a Fight class in Java, and a Fight script which can be run in Ruby.
Class Diagram
Java
Fighter.java:
package csc517.wiki1b; public abstract class Fighter { private Attack attack; public Fighter(){ setAttack(); } public Attack getAttack() { return attack; } public void setAttack(Attack attack) { this.attack = attack; } // This method sets the fighter's default attack, // which will be different for each type of fighter protected abstract void setAttack(); public void fight() { getAttack().execute(); } }
JudoPlayer.java:
package csc517.wiki1b; public class JudoPlayer extends Fighter { public void setAttack() { // The JudoPlayer will execute a LegSweep by default setAttack(new LegSweep()); } }
KickBoxer.java:
package csc517.wiki1b; public class KickBoxer extends Fighter { public void setAttack() { // The KickBoxer will execute a RoundhouseKick by default setAttack(new RoundhouseKick()); } }
Boxer.java:
package csc517.wiki1b; public class Boxer extends Fighter { public void setAttack() { // The Boxer will execute a Jab by default setAttack(new Jab()); } }
Wrestler.java:
package csc517.wiki1b; public class Wrestler extends Fighter { public void setAttack() { // The Wrestler will execute a ShootIn by default setAttack(new ShootIn()); } }
Attack.java:
package csc517.wiki1b; public interface Attack { public void execute(); }
RoundhouseKick.java:
package csc517.wiki1b; public class RoundhouseKick implements Attack { public void execute() { System.out.println("Roundhouse Kick!"); } }
ShootIn.java:
package csc517.wiki1b; public class ShootIn implements Attack { public void execute() { System.out.println("Shoot In!"); } }
LegSweep.java:
package csc517.wiki1b; public class LegSweep implements Attack { public void execute() { System.out.println("Leg Sweep!"); } }
Jab.java:
package csc517.wiki1b; public class Jab implements Attack { public void execute() { System.out.println("Jab!"); } }
Fight.java:
package csc517.wiki1b; public class Fight { public static void main(String[] args) { // Create two fighters with default attack types Fighter[] fighters = { new Boxer(), new Wrestler() }; // Fight three rounds for (int round = 0; round < 3; round++) { for (int fighter = 0; fighter < fighters.length; fighter++) { fighters[fighter].fight(); } } // Now swap in new behaviors fighters[0].setAttack(new LegSweep()); fighters[1].setAttack(new RoundhouseKick()); for (int fighter = 0; fighter < fighters.length; fighter++) { fighters[fighter].fight(); } } }
Ruby
Fighter.rb:
class Fighter attr_accessor :attack def initialize(technique) @attack = technique end def fight @attack.execute end end
JudoPlayer.rb:
require 'Fighter' require 'LegSweep' class JudoPlayer < Fighter def initialize # The JudoPlayer will execute a LegSweep by default super(LegSweep.new) end end
KickBoxer.rb:
require 'Fighter' require 'RoundhouseKick' class KickBoxer < Fighter def initialize # The KickBoxer will execute a RoundhouseKick by default super(RoundhouseKick.new) end end
Boxer.rb:
require 'Fighter' require 'Jab' class Boxer < Fighter def initialize # The Boxer will execute a Jab by default super(Jab.new) end end
Wrestler.rb:
require 'Fighter' require 'ShootIn' class Wrestler < Fighter def initialize # The Wrestler will execute a ShootIn by default super(ShootIn.new) end end
RoundhouseKick.rb:
class RoundhouseKick def execute puts "Roundhouse Kick!" end end
ShootIn.rb:
class ShootIn def execute puts "Shoot In!" end end
LegSweep.rb:
class LegSweep def execute puts "Leg Sweep!" end end
Jab.rb:
class Jab def execute puts "Jab!" end end
Fight.rb:
# Create two fighters with default attack types rounds = 3 fighters = [ JudoPlayer.new, KickBoxer.new ] # Fight three rounds rounds.times do fighters.each do |fighter| fighter.fight end end # now swap in new behaviors fighters[0].attack = ShootIn.new fighters[1].attack = Jab.new fighters.each do |fighter| fighter.fight end
Comparison of Implementations
Clarity
Both languages can do the basic implementation of the design pattern framework with near equal clarity. The Java implementation relies less on language idioms, making it somewhat more clear (but not always). For example, to anyone familiar with O-O languages (but not necessarily Java), it is clear what each of the concrete Fighter types does, as well as the Attacks. However, the abstract Fighter class might not be as obvious (but probably still should be).
Succinctness
Overall, the two implementations are fairly similar in their succinctness, but in general, the Ruby implementation is more concise. Ruby's built-in iterators make it easier to work with collections, like arrays of Fighters, or rounds in a Fight. Also, the Ruby version contains fewer lines of code than the Java implementation. If you ignore packaging (Java package statements) though, there are actually more lines in the Ruby implementation. However, there are many features of the Ruby language that could be used to make the implementation more concise than than the Java version. For example, Ruby offers a means to simplify the code by using its block construct and Proc objects. This will work for simple strategy patterns and could allow for the elimination of the behavioral classes altogether. To make a comparison with Java, the Java strategy pattern would need to be implemented using anonymous inner classes, in which case Ruby's more versatile block construct would be easier to implement and maintain.
The real advantage to using Ruby becomes more apparent when you extend the design to model more real-world situations. Then you can take advantage of its dynamic nature. For example, let's consider when a Fighter wants to learn new techniques, and be able to choose which one to use. In Java, you need to create new Attack implementations before you can use them in a Fight. Then you need to create a mechanism to choose which attack to use. The block idiom in Ruby would allow new attack behaviors to be created on the fly at run time in addition to being assigned at compile time. This meta-programming capability allows flexibility that Java does not support. Also, if you wanted to combine several attacks into an arsenal from which the fighter could choose any attack, this could be easily accomplished using Ruby's arrays and built-in iterators.
Concluding Remarks
It would be nice to be able to say that one implementation was always clearer or more succinct than the other, but as with most things, it is a compromise. To get a little more clarity in Java, you sacrifice some succinctness, and in Ruby you can get more succinctness for the price of some clarity. Choosing a design implementation may come down to familiarity with the language or design decisions outside the scope of this discussion.
References
Design Patterns, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Addison-Wesley Pulishing, 1995, pp. 315-323
Refactoring: Improving the Design of Existing Code, Martin Fowler, Addison-Wesley Publishing, 1999
Head First Design Patterns, Eric Freeman and Elisabeth Freeman with Kathy Sierra and Bert Bates, O'Reilly Media, 2004
Programming Ruby, Dave Thomas with Chad Fowler and Andy Hunt, The Pragmatic Programmers, 2005
CSC/ECE517 Lecture Notes (lecture 10), Edward F. Gehringer, 2007
Command vs Strategy Pattern? (OO, Patterns, UML and Refactoring forum at JavaRanch)
Rubynoob Strategy Design Pattern in Ruby
Re: Head First Design Patterns - Strategy Pattern [was: Java/C# "interface" in Ruby ?]