Tech Book Face Off: Design Patterns in Ruby Vs. Practical Object-Oriented Design in Ruby

I've been in a good book-reading mood lately, so I'm writing up yet another Tech Book Face Off. This time I wanted to dig into some more Ruby books, since I've felt like I still have much to learn about this wonderful programming language. I also wanted to work on writing better organized programs, so I targeted some books on program design. The books on deck are Design Patterns in Ruby by Russ Olsen and Practical Object-Oriented Design in Ruby by Sandi Metz. Let's see how they compare with each other and with some of the other books I've read on design.

Design Patterns in Ruby front coverVS.Practical Object-Oriented Design in Ruby front cover

Design Patterns in Ruby


This is probably my fifth book on design patterns, after the original Design Patterns, Agile Principles, Patterns, and Practices in C#, Refactoring to Patterns, and recently, JavaScript Patterns. Let me tell you, enough is enough. I'm not reading anymore books on patterns. Don't read this many books on design patterns—it's just not worth it. I would recommend that if you're mostly programming in C#, JavaScript, or Ruby, read the above patterns book for that language. The examples will directly apply to your coding, and whatever patterns were left out probably aren't important anyway. If your main language is something else that's not C++, any of these books will probably do fine, but skip Refactoring to Patterns. The content was too obvious, and probably not worth trudging through the book. And if you are programming in C++, the original Design Patterns (known as Gang of Four, or GoF) will probably work for you, even if the code examples are a little dated.

Having said all of that, Design Patterns in Ruby is not a bad book. Russ Olsen is an excellent writer, and his book is quite clear and easy to read. I could zip right through the book because his prose and code examples flowed smoothly and were easily understood. He starts out with an introductory chapter on how patterns should and shouldn't be used, followed by a chapter that quickly covers Ruby if you're getting started with the language. Sometimes I wonder if I could pick up a new language by reading a book like this in a language I don't already know, making this intro-to-the-language chapter useful, but I normally know a language before reading these intermediate level books so I find these language chapters superfluous.

The rest of the book is all about patterns—a chapter for each of 13 classical patterns and 3 chapters at the end for new Ruby-specific patterns. Each pattern chapter follows the general flow of introducing a design problem with a suboptimal code solution, explaining how to solve the problem better with a design pattern, improving on that solution with Ruby-specific features, advising on how not to abuse the pattern, and giving some examples of real world code that uses the pattern.

Many of the patterns, especially at the beginning of the book, are trivial to implement in Ruby because of specific Ruby features like blocks, dynamic typing, and metaprogramming. Olsen nicely describes why Ruby is so good for implementing these patterns:
The Ruby programming language takes us a step closer to my old friend's ideal, a language that makes implementing patterns so easy that sometimes they fade into the background. Building patterns in Ruby is easier for a number of reasons: Ruby is dynamically typed. By dispensing with static typing, Ruby dramatically reduces the code overhead of building most programs, including those that implement patterns. Ruby has code closures. It allows us to pass around chunks of code and associated scope without having to laboriously construct entire classes and objects that do nothing else.
Some patterns can be implemented in dramatically less code while being even more flexible than the original patterns were because of how flexible the Ruby language is. Because of duck typing, the Template Method pattern can be implemented without creating an inheritance hierarchy. Code blocks make the Strategy pattern trivial to use, and it becomes a matter of identifying uses of code blocks as the Strategy pattern by intent as much as by any specific structure. The Observer pattern can be used simply by mixing in a module, and blocks further ease its implementation. The Iterator pattern is embedded deep within Ruby with the Enumerable module, and using it with collection classes is very idiomatic in Ruby code. Finally, numerous patterns like Proxy, Factory, and Builder can be implemented using method_missing and other meta-programming techniques, substantially cutting down the amount of boilerplate code needed to use these patterns.

All of these Ruby features that make using patterns easier are great, and you may already be using a bunch of patterns in your code without even realizing it. That doesn't mean that you should go out of your way to use more patterns, though. Some patterns still require a fair amount of code to implement, and they end up being a big (and slow) hammer when you're dealing with a small problem. Olsen has some great advice for why you should wait to inject a pattern into your code:
First, you are betting that you will eventually need this new feature. If you make your persistence layer database independent today, you are betting that someday you will need to port to a new database. If you internationalize your GUI now, you are betting that someday you will have users in some foreign land. But as Yogi Berra is supposed to have said, predictions are hard, especially about the future. If it turns out that you never need to work with another database, or if your application never makes it out of its homeland, then you have done all of that up-front work and lived with all of the additional complexity for naught.
Not needing to use something that took more time and complicated your codebase is bad enough. His second point is even more insightful:
The second prong of the bet that you make when you build something before you actually need it is perhaps even more risky. When you implement some new feature or add some new flexibility before its time, you are betting that you can get it right, that you know how to correctly solve a problem that you haven’t really encountered yet.
These aren't new warnings. Every book that I've read on patterns and programming practices has similar advice, but it can't be repeated enough. Programmers seem to have a propensity for complexity. It's exciting. It's challenging. It boosts our egos. We always need to be aware of when we're going too far so we can temper our enthusiasm for big, complicated code monstrosities.

As if to heed his own advice, Olsen omitted nine patterns from the original GoF patterns: Prototype, Bridge, Facade, Flyweight, Chain of Responsibility, Mediator, Memento, State, and Visitor. Most of these patterns are either trivial, combinations of other patterns, or not used very often. The only one that I kind of object to leaving out is the State pattern. I use this pattern a lot, although not with the implementation of the original GoF treatment. The GoF implementation is a bit over-engineered for most cases, with a class defined for each state in a state hierarchy. I normally use a simple switch statement, an enumeration of states, and a state variable within an instance method, but the State pattern is incredibly useful. I would have like to see Olsen's take on it in Ruby.

All in all, Design Patterns in Ruby was a decent book on design patterns. It's a bit hard for me to review it objectively at this point, due to my pattern-weariness, but I could still recommend it for those Ruby programmers that haven't read a patterns book, yet. If you have, then I'd give this book a pass. There are plenty of other programming books that are more worth your time.

Practical Object-Oriented Design in Ruby


Like Design Patterns in Ruby, this book is similar to quite a few other books I've read on software design. Sandi Metz doesn't really get into design patterns—covering only the Template Method, Factories, and Composition patterns while not really focusing on them—and instead concentrates on other aspects of good object-oriented design. She covers the SOLID principles without referring to them directly as such, and dabbles in some of the UML diagrams without going into tremendous detail.

Practical Object-Oriented Design in Ruby is generally a subset of Agile Principles, Patterns, and Practices in C#, giving a lighter treatment of software design using the beautiful, concise code examples that you get with Ruby. Metz forgoes the tiresome chapter on introducing the Ruby language so it's expected that you have some familiarity with the language, but she doesn't go deep into Ruby arcana so anyone well-versed in an object-oriented language should be able to understand the examples without any issues. The advice throughout the book is certainly applicable to any object-oriented language.

The book starts out with an introductory chapter on what object-oriented design is before diving into the first SOLID principle in chapter two on the Single Responsibility Principle. This principle is easy to describe, but can be hard to follow. A good way to know if a class has a single responsibility is to describe it:
If the simplest description you can devise uses the word “and,” the class likely has more than one responsibility. If it uses the word “or,” then the class has more than one responsibility and they aren’t even very related.
If a class has more than one responsibility it should either be broken up into classes that do have single responsibilities or generalized so that it can be used in a consistent and singular way. This is a core concept in object-oriented design that dramatically simplifies a program's structure and makes it easier to understand by breaking it up into much more manageable layers and components. This same principle also applies to methods. Every method should do one thing, and do it well.

This same chapter had some good advice on how to use arrays. Because Ruby allows mixed-type arrays, it's quite easy to abuse them and start throwing all kinds of objects into arrays of configuration values to pass around to other objects and methods. But that leads to problems:
It depends upon the array’s structure. If that structure changes, then this code must change. When you have data in an array it’s not long before you have references to the array’s structure all over. The references are leaky.
Wherever the code accesses elements within the array, it needs to know where stuff is, and that leads to nasty dependencies strewn throughout the code. It's best to use more well-defined structures for collections of configuration values—at least a hash with named keys if not a struct or a full-blown object.

The next chapter deals with dependencies, how to identify them, and how to reverse them. After that we move on to creating flexible interfaces. This is where Metz really gets into how to divide up a program into the simple objects needed to elegantly solve the problem at hand, and she has some nice discussions about how to go about doing this:
Domain objects are easy to find but they are not at the design center of your application. Instead, they are a trap for the unwary. If you fixate on domain objects you will tend to coerce behavior into them. Design experts notice domain objects without concentrating on them; they focus not on these objects but on the messages that pass between them. These messages are guides that lead you to discover other objects, ones that are just as necessary but far less obvious.
Messages are key to object-oriented design, and Ruby does a great job of bringing messages to the forefront of the code. Because Ruby is a dynamic language, as long as an object responds to a given message (as a method call), you can send it the message, regardless of whether the class of the object is what you expected it to be. This concept is called duck typing, and the next chapter covers the design aspects associated with duck typing in detail.

After duck typing, we finally come to a chapter on what everyone expects to see in object-oriented programming, inheritance. This chapter covers how you would go about designing an inheritance hierarchy for a simple application, and Metz cautions the reader that inheritance is a powerful tool that can be easily abused, especially in Ruby with the combination of duck typing and a flexible method call resolution system:
You can write code that is impossible to understand, debug, or extend. This is powerful stuff, and dangerous in untutored hands. However, because this very same power is what allows you to create simple structures of related objects that elegantly fulfill the needs of your application, your task is not to avoid these techniques but to learn to use them for the right reasons, in the right places, in the correct way.
Inheritance hierarchies should be small and simple, not too deep or excessively wide. Just because you can create a massive 10-level hierarchy of the animal kingdom for your zoo application doesn't mean you should. You really shouldn't. Metz does a good job of explaining all of the traps and complications involved in poorly-designed inheritance hierarchies.

In a similar style of exposing trade-offs, the next couple chapters cover module design and using composition to build more complex objects. Then the final chapter spends a fair amount of space discussing how to design the tests for your code. Metz throws out the obligatory justifications for writing tests:
The most common arguments for having tests are that they reduce bugs and provide documentation, and that writing tests first improves application design.
While these arguments are well-known, I think the last one is the weakest of the three. Tests can improve an application's design in certain ways for certain programmers, but I wouldn't say it's universally true. I've definitely come across instances of code that had to be changed in special ways specifically to support testing without improving the design of the code at all. It's not a huge deal, though, to have some exceptions to the argument, and Metz has a refreshingly pragmatic stance on testing in general. She advocates writing simple tests that only exercise the public interfaces and to be sure to not duplicate testing. I found myself agreeing with most of her recommendations.

Overall, Practical Object-Oriented Design in Ruby was a pleasant, easy read. Even though it was fairly short, it did get redundant sometimes and explanations tended to drag on. Part of that may have been me thinking ahead and wanting to get on to other things since I've read so many books with similar concepts to those covered here. I've seen pretty much all of this material in other books like Object-Oriented Design Heuristics, The Pragmatic Programmer, and Code Complete, as well as the other books mentioned in this review. They're all good books, and I would think if I had come across POODR earlier, I would have been more excited about it. If you haven't already read too many other books on design, this is a good one to check out.

How Much Design is Enough?


I've definitely read too many books on object-oriented design and design patterns, but how much is enough? Well, a pair of each seems like a good number. I like getting more than one perspective on a subject because it adds significantly to my understanding, both because different authors will cover somewhat different material and because going over something a second time helps cement it in my brain. For design patterns, I'd probably recommend Design Patterns in Ruby and Agile Principles, Patterns, and Practices in C# as the best, but if you're primarily a JavaScript programmer, then JavaScript Patterns would be a better choice.

My favorites for object-oriented design would be Object-Oriented Design Heuristics and The Pragmatic Programmer, although the latter is not so much about object-oriented design as general advice on how to be a great programmer. If you're primarily a Ruby programmer, maybe Practical Object-Oriented Design in Ruby would be for you, but I really liked Eloquent Ruby, which also covers a lot of object-oriented design and goes much deeper into writing well in Ruby.

The important thing to realize is that at some point you're going to saturate on reading about this stuff, and you need to stop reading and start practicing. That's where I'm at. I have a good grasp of the concepts. Now I need to knuckle down and continue improving my programming skills. I won't be doing that by reading more books. I have to actually put all of that knowledge I've accumulated to work.

No comments:

Post a Comment