As I mentioned in my last post (
Ruby Matters: Meta-programming, Synthesis, and Generation), the Gang of Four design patterns book should have been named "Palliatives for C++". One of the authors finally admitted as much in public at a roast. So, why would design patterns be any different in a dynamic language (like Smalltalk or Ruby)?
In the GoF book, design patterns are 2 things: nomenclature and recipes. The nomenclature part is useful. It's a way of cutting down on repetition of similar code across projects, and it gives developers a way to talk to one another, using shorthand. Instead of saying "I need to create an object that can only be instantiated once", you say "singleton" (we'll leave aside for the moment why Singleton is evil -- the subject of another blog entry). Nomenclature good.
But the fatal flaw in the GoF book was that they included recipes. And many people thought they were the best part. Even now, you see books on Java design patterns that blindly mimic the structure of the examples in the GoF book (even though Java has some better mechanisms, like interfaces vs. pure virtual classes). Recipes bad. Because they suggest more than just a way to name common things. They imply (and put in you face) implementation details.
Because of meta-programming, many of the design patterns in the GoF book (especially the structural ones) have much simpler, cleaner implementations. Yet if you come from a weaker language, your first impulse is to implement solutions just as you would from the recipe.
Here's an example. Let's say you have a simple
Employee
class, with a few fields (which could be properties in the Java sense, but it makes no difference for this example).
public class Employee {
public String name;
public int hireYear;
public double salary;
public Employee(String name, int hireYear, double salary) {
this.name = name;
this.hireYear = hireYear;
this.salary = salary;
}
public String getName() { return this.name; }
public int getHireYear() { return this.hireYear; }
public double getSalary() { return this.salary; }
}
Now, you need to be able to sort employees by any one of their fields. This is a flavor of the Strategy Design Pattern: extracting an algorithm into separate classes so that you can have different flavors. Java already includes the basic mechanism for this, the
Comparator
interface. So, here's what the code to be able to compare on any field looks like in Java:
public class EmployeeSorter {
private String _selectionCriteria;
public EmployeeSorter(String selectionCriteria) {
_selectionCriteria = selectionCriteria;
}
public void sort(List<Employee> employees) {
Collections.sort(employees, getComparatorFor(_selectionCriteria));
}
public Comparator<Employee> getComparatorFor(String field) {
if (field.equals("name"))
return new Comparator<Employee>() {
public int compare(Employee p1, Employee p2) {
return p1.name.compareTo(p2.name);
}
};
else if (field.equals("hireYear"))
return new Comparator<Employee>() {
public int compare(Employee p1, Employee p2) {
return p1.hireYear - p2.hireYear;
}
};
else if (field.equals("salary")) {
return new Comparator<Employee>() {
public int compare(Employee p1, Employee p2) {
return (int) (p1.salary - p2.salary);
}
};
}
return null;
}
}
You might protest that this is overly complicated, and that this will do the job:
public Comparator<Employee> getComparatorFor(final String field) {
return new Comparator<Employee>() {
public int compare(Employee p1, Employee p2) {
if (field.equals("name"))
return p1.name.compareTo(p2.name);
else if (field.equals("hireYear"))
return p1.hireYear - p2.hireYear;
else if (field.equals("salary"))
return (int) (p1.salary - p2.salary);
else
// return what? everything is a legal value!
}
}
}
but this one won't work because you must have a return, and returning any value is mis-leading (every possible integer here means something: 1 for greater than, 0 for equal, -1 for less than). So, you are left with the bigger version. I've actually attempted to optimize this several ways (with more generics, reflection, etc. but I always get defeated by the requirement to return a meaningful
int
from the comparison method. You could find some kind of minor optimization, but you are still building structure to solve the problem: a class per comparison strategy. This is the typical
structural approach to solving this problem.
Here's the same solution in Ruby, using a similar
Employee
class:
class Array
def sort_by_attribute(sym)
sort {|x,y| x.send(sym) <=> y.send(sym) }
end
end
In this code, I'm taking advantage of several Ruby features. First, I'm applying this to the
Array
class, not a separate
ComparatorFactory
. Open classes allow me to add methods to built-in collections. Then, I'm taking advantage of the fact that Ruby method calling semantics are message based. A method call in Ruby is basically just sending a message to an object. So, when you see
x.send(sym)
, if
sym
has a value of
age, we're calling
x.age
, which is Ruby's version of the accessor for that property. I'm also taking advantage of the Ruby "spaceship" operator, which does the same thing that the Java
compare
method does: return a negative if the left argument is less than the right argument, 0 if they are equal, and positive otherwise. Wow, it's nice to have that as an operator. I should add that to Java...oh, wait, I can't. No operator overloading.
Perhaps it makes you squeamish to add this method to the
Array
class (open classes seem to terrify Java developers a lot). Instead, in Ruby, you could add this method to a particular
instance of array:
employees = []
def employees.sort_by_attribute(sym)
sort {|x, y| x.send(sym) <=> y.send(sym)}
end
Now, the improved employees array (and only this instance of array) has the new method. A place to put your stuff, indeed.
When you have a powerful enough language, many of the design patterns melt away into just a few lines of code. The patterns themselves are still useful as nomenclature, but not as implementation details. And that's the real value. Unfortunately, the implementation seems to be the focus for many developers. Strong languages show you that the implementation details can be so trivial that you hardly even need the nomenclature (but its still useful as a conversation shortener.)
Next, I'll talk about language beauty.