I can understand their chagrin. How would you like it if you just discovered something cool only to have some spoilsport tell you that roughly half of it is crap? Yet I just can't keep quiet; partly because I'm mean and love seeing their reactions, but mostly because I'm sick of the hype.
Patternitis
"The whole problem with the world is that fools and fanatics are always so certain of themselves, but wiser people so full of doubts."
Bertrand Russell
Don't get me wrong, I think design patterns are a great idea. Hardly a new one, but great nevertheless. The problem is that every Great Idea attracts its share of fanatics who think it's the coolest thing since sliced bread. It gets paraded around as a Silver Bullet and a Golden Hammer until its limitations percolate through the layers of hype. Inevitably, the general hype dies down -- if we're talking about a genuine Great Idea, not glossyware stuff like SOA -- and all that's left are pockets of hype that form about enthusiastic newbies or incompetent know-it-alls.
I personally love stamping out those little pockets and that's what I want to do with this blog post. I'll start by asserting the following:
- If you think that developing software can be reduced to applying and combining design patterns, you are wrong.
- If you think that a design pattern you don't completely understand must nevertheless be good, you are wrong.
- If you think that design patterns are a new invention made possible by OOP, you are wrong.
- If you think that someone is a good programmer just because they know design patterns, you are wrong.
- If you think that you must know design patterns to be a good programmer, you are wrong.
Gold vs. Other Glittery Stuff
"Any man whose errors take ten years to correct is quite a man."
J. Robert Oppenheimer
J. Robert Oppenheimer
What is a design pattern? The original book by Gamma, Helm, Johnson and Vlissides (popularly known as GoF) takes a page and a half to answer this question. I prefer a shorter and simpler answer, even if it lacks all the accuracy of the original. Wikipedia has pretty good definition:
In software engineering (or computer science), a design pattern is a general repeatable solution to a commonly occurring problem in software design. A design pattern is not a finished design that can be transformed directly into code. It is a description or template for how to solve a problem that can be used in many different situations. Object-oriented design patterns typically show relationships and interactions between classes or objects, without specifying the final application classes or objects that are involved. Algorithms are not thought of as design patterns, since they solve computational problems rather than design problems.Still a bit long for my taste, but it's quite usable. As a matter of fact, the first sentence is the most important; the rest just helps cement the scope.
The GoF book defines 23 design patterns: 5 creational, 7 structural and 11 behavioral. It took me a lot of conscious effort and quite a few years of working experience to admit that not all of those patterns are gold. In fact, the wheat-to-chaff ratio is quite close to 50 percent: there are 12 that I could describe as "okay", "good" or "great" and 11 that I see as "hack", "poor" or even "rubbish".
The Good
"The remarkable thing about Shakespeare is that he really is very good, in spite of all the people who say he is very good."
Robert Graves
Robert Graves
If I learned something from my previous managers, it's to always dangle the carrot before using the stick. Seriously, though, the original GoF book has some really good patterns in it.
Take Bridge, for example. The best thing about this pattern is that it teaches you about the separation of concerns. You can adapt it to classless object-oriented languages, you can even apply it in structural programming languages. The main point is to keep the implementation primitives separate from the operations that use them.
Builder is another example of a highly useful pattern. Just look at SAX: they provide the director and you have to write the builder. Highly efficient and very elegant. By the way, I really recommend reading the original GoF book if you want to understand this pattern. Some of the other books attempt to explain it by applying it to convoluted problems such as constructing a MIME message or building a user interface. A word of advice to the authors: KISS.
A few other great patterns include Command, Composite and Template Method. These really make the best out of OOP and it shows. These are all nice examples of taking advantage of the benefits of OOP.
Then there is a whole range of patterns that have been around for a long time, long before OOP and long before GoF invented design patterns as we know them: Adapter, Façade, Flyweight, Chain of Responsibility and Observer.
Finally, I reserve a special place for Mediator, not because it's better than the rest, but because it's a perfect example of a good pattern that can be horribly abused. One word: Delphi. The "RAD paradigm" promoted by Delphi is to add objects to their containers and then write all the logic in a few huge Mediator classes. Delphi is based on an object-oriented language, has an excellent core framework and class library and yet it teaches its users to forget OOP. Bottom line: no matter how cool the pattern, don't overdo it.
The Ugly
"Beauty is the first test; there is no permanent place in the world for ugly mathematics."
G.H. Hardy
G.H. Hardy
We have all, at some point in our lives, had to choose the lesser of two evils. That's most likely what you will be doing when you decide to apply one of the following: Proxy, Memento, Visitor, Decorator or State. All of these are a cumbersome solution for something that should have been supported or aided by the language itself.
Proxy, for example, is a necessary and useful pattern. The problem with it is that a lot of the languages require you to implement it by writing tons of boilerplate code. With some good metaprogramming facilities, Proxy becomes trivial.
Likewise, the motivation behind Memento is valid, but it should be solved by the language or its core library. Serializing and deserializing objects should be trivial, which means you shouldn't have to write gobs of code to do it. Look at the way you do it in Java:
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(oos); try { oos.writeObject(originator); oos.close(); } catch (IOException e) { // won't happen but we _still_ // have to write code to ignore it } byte[] memento = baos.toByteArray();Now compare it to Ruby:
memento = Marshal.dump(originator)I rest my case.
Visitor is another example of hacking your way around language limitations. Most of the object-oriented code anyone writes needs only single dispatch. You can argue that supporting double or multiple dispatch in the language is wasteful, although there are ways around that. But when your language leaves you with no other choice and you need double dispatch, you will have to use Visitor. If you happen to need multiple dispatch, good luck writing and maintaining that code.
Finally, we come to Decorator and State. Both of these patterns are useful and both deal with the same issue: sometimes we want to adapt the behavior of individual objects depending on their state or some other trait we want to manipulate during run time. I believe that it should be possible without writing all the boilerplate code that you need for Decorator and State. I've learned and experimented with Self and Io languages and they both allow you to solve these needs very elegantly. Again, look at a sample implementation of State pattern in Java:
public class Bot { private int health; private BotState state; protected void changeState(BotState newState) { state = newState; } protected static final BotState HEALTHY = new HealthyState(); protected static final BotState INJURED = new InjuredState(); protected abstract static class BotState { public void takeDamage(Bot bot, int damage) { bot.health -= damage; } public void heal(Bot bot, int delta) { bot.health += delta; } public void enemySpotted(Bot bot, Object enemy) {} } protected static class HealthyState extends BotState { public void takeDamage(Bot bot, int damage) { super.takeDamage(bot, damage); if (bot.health <= 40) { bot.changeState(INJURED); } } public void enemySpotted(Bot bot, Object enemy) { bot.chase(enemy); } } protected static class InjuredState extends BotState { public void heal(Bot bot, int delta) { super.heal(bot, delta); if (bot.health >= 50) { bot.changeState(HEALTHY); } } public void enemySpotted(Bot bot, Object enemy) { bot.avoid(enemy); } } public void chase(Object enemy) { System.out.print("Chasing " + enemy); } public void avoid(Object enemy) { System.out.print("Avoiding " + enemy); } public void takeDamage(int damage) { state.takeDamage(this, damage); } public void heal(int delta) { state.heal(this, delta); } public void enemySpotted(Object enemy) { state.enemySpotted(this, enemy); } }Now compare it with the same thing in Io:
Bot := Object clone do ( init := method(self health := 100) chase := method(enemy, ("Chasing " .. enemy) println) avoid := method(enemy, ("Avoiding " .. enemy) println) takeDamage := method(damage, health = health - damage) heal := method(delta, health = health + delta) enemySpotted := method(enemy, nil) changeState := method(newState, self setProto(newState)) ) HealthyBot := Bot cloneWithoutInit do ( takeDamage := method(damage, resend if (health <= 40, changeState(InjuredBot)) ) enemySpotted := method(enemy, chase(enemy)) ) InjuredBot := Bot cloneWithoutInit do ( heal := method(delta, resend if (health >= 50, changeState(HealthyBot)) ) enemySpotted := method(enemy, avoid(enemy)) )I would even settle for hard, specific syntax supported by the language itself, like in UnrealScript.
Bottom line? Our languages really need to evolve. Combine prototype-based OOP that you can find in Self and Io with excellent metaprogramming offered by Lisp and you will have a language that kicks ass and doesn't need ugly patterns.
The Bad
"There is no stigma attached to recognizing a bad decision in time to install a better one."
Laurence J. Peter
Laurence J. Peter
Thus we come to the place where the bad things dwell. There's a variety of patterns that fall into this group and they do so for diverse reasons. Let's start with a couple that gave me nightmares for a long while.
Factory Method sounds like a good idea. Upon close examination, Abstract Factory is revealed to be Factory Method applied to Factory Method, which makes it sound even better. Only those who have been fooled by this pattern can appreciate the despair and the subsequent numbness brought on by writing tons and tons of the boilerplate code required by it. The worst thing is that it seems to be the only solution. Until you find out about Dependency Injection. Try it and you'll see the difference.
Another pattern that really fouls things up is Singleton. For a long time I couldn't find one good reason why I should have a class with only one instance, instead of making all that code static. Then someone asked what I would do if I had to pass an interface to it. Okay, that justifies it, I can even think of examples other than boilerplate Abstract Factory code. Another valid point is that you might want to switch from one instance to a pool and you don't want to rewrite the bulk of your code. Then there is the case mentioned in the GoF book that talks about subclassing the singleton and creating a registry of singletons. If you think that things are getting out of hand, you're right. The solution is the same as above: use Dependency Injection.
Then there's Iterator pattern. That one should be easy: use the foreach statement. And if your language doesn't have it, ask for your time back. It's simply unacceptable to have to write reams of ugly code for something we use so often. It deserves its own syntax.
What about Strategy pattern? According to the GoF and to all subsequent authors, you should use it to encapsulate a family of algorithms and make them interchangeable. That's about one thing for which I would not use it. If you want to do that use functions. Again, if you don't have them, ask for your time back. On the other hand, you could use Strategy pattern when your implementation strategy has to maintain state over time. But then it's not really Strategy, is it? Slippery things, these patterns.
Finally, there's Interpreter. I don't have any beef with its motivation: just look at how things have been evolving and you'll see several examples of minilanguages, scripting languages and XML-based stuff, all getting interpreted in the normal course of execution of your code. From printf format string of olden to XML configuration files of today, the idea has been in use for a loooong time. Thing is, this isn't a design pattern. It doesn't fit into the scope of a design pattern. And you won't make it fit by sketching out your favorite implementation of an interpreter for your favorite minilanguage. It just does not belong.
Patternitis Revisited
"When people are free to do as they please, they usually imitate each other."
Eric Hoffer
Eric Hoffer
I have to give it to GoF though, they had some great ideas and the only time their scope slipped was with the Interpreter pattern. Sadly, the same couldn't be said about the later authors. I'll say it out loud: interface is not a design pattern. There are many things you could say about an interface. From a conceptual point of view, it's the definition of the contract your code will have to satisfy. From a language-specific point of view, it's a syntactic and semantic construct. But it is definitely not a design pattern. Neither is a symbolic constant name or an accessor method name.
It might seem that I'm beating a dead horse here, but I want to make it absolutely clear that not everything in software development is a design pattern. It's too easy to lose focus in enthusiasm, so do yourself a favor: learn the design patterns, keep using the ones you like (where applicable) and move on. There's a lot more to do.
No comments:
Post a Comment