This 1 Weird Trick to Almost Any Design Pattern!

I was answering a question on Programmers.SE the other day, as I am wont to do from time to time, and something interesting struck me. As I was constructing my answer, I found myself looking for a design pattern to name that would be apropos for the situation (I settled on the command pattern), and while I was grasping for the right one, I thought, I want the one where we put an object in between. It hit me: that's almost all of them!

That's right, nearly every design pattern is basically just "create an intermediate object to encapsulate what you want." For example, we have the command pattern: create an object that calls behavior in the way that we want. We have the adapter pattern: create an object that transforms what we have to what we want. We have the strategy pattern: create an object that will polymorphically perform a behavior we want.

As I realized that, I felt like, while design patterns offer a great way to name and model a certain solution when you need it, the truth is that we can just think of creating intermediate objects and letting the specific names fall away. The names can still provide a way to convey what we are doing, but we can realize and attach them after the fact. Rather than saying, "I need an adapter pattern here!" instead, we can say, "I need an intermediate object here!" and not be constrained by strict adherence to the patterns.

This both reinforces and strikes at the heart of some criticisms of design patterns. Of course they're just object-oriented design. They're all about objects and creating objects. Or perhaps instead, they're all about making the objects fall away and focusing on the messages and the behaviors we really want. The fact that some of them fade into the background in different languages speaks to the fact that they are inherent and transparent. Those languages don't prohibit the patterns, they enable them transparently through language constructs.

But...


There are two patterns that do not strictly follow the "just create an intermediate object" mantra: the singleton and the visitor. These two are different in some ways. Does this mean they are not real patterns? Does this mean they are anti-patterns? The singleton is considered an anti-pattern by many. It is hard to test and is often equated with global state (rightly so). But with the context of patterns being "create an intermediary object," we can clearly see that we don't do that with the singleton. Indeed, the singleton is all about not creating objects. It breaks our polymorphic transparency because we have a much harder time creating and using different implementations. It is an anti-pattern because we lose the object-oriented-ness.

The visitor pattern has the unique trick of being able to recover runtime type through polymorphism. We create visitor objects to encapsulate the behavior we want to see, but we do so through "double-dispatch". The visitor pattern must also be implemented in a rather cumbersome way by repeating the same accept method on every different implementation. These are all signals that the visitor pattern is different. So is it an anti-pattern?

I would sat that the visitor pattern is a completely different beast, both from the other design patterns and from the anti-patterns. By recovering runtime types, it works to reverse type abstraction rather than rely on abstraction. It does not preclude the creation of objects, but it does not solve its problem by encapsulating with them either. To me, visitors have a different lineage than the rest of the patterns. It's in the same family, and shares some resemblance, but it is a different kind of pattern. To see what I mean, let's look at the following Ruby code:

['a', 'b', 'c'].map do |letter|
  letter.upcase
end


This code takes each element of the array, and upper-cases it (returning a new array). We do this by calling the map method with a block. The block is a visitor in many ways. Rather than accept, we have map, as well as many other methods that take blocks. These methods are on every collection in Ruby, whether they be arrays, linked lists, generators, or whatever else that can be abstracted as a collection. The methods take the block, an object consisting of a single behavior, and call that block with each element in the collection (through the yield keyword). This again is double-dispatch, the hallmark of the visitor pattern. Rather than recovering type, though, we are recovering collected data (a twist on the pattern).

The use of methods like map, each, and reduce is often compared to functional programming. I am not going to assert that the visitor pattern is functional programming, but we can see some shared ideas. The visitor pattern is about separating data (the runtime type, the encapsulated state) in the visited object from the behavior in the visitor object. It treats the data-divorced behavior as a first-class entity, and allows us to use it as a parameter or even a return type. Perhaps we can think of the visitor as a kind of hybrid, or a way to bring some functional ideas into the object-oriented realm. It's a pattern, but it's a little different.

Comments

Popular posts from this blog

The Timeline of Errors

Magic Numbers, Semantics, and Compiler Errors

Assuming Debugging