Although the book is totally focused on Ruby, the OO practices it presents are easily applicable to other OO languages, including static languages like C#. This makes it one of those timeless books you can be happy to have on your shelf knowing it's not going to be outdated in a year. I highly recommend it!
I intend to write a few posts highlighting some of the good ideas that struck me the most from this book, but in this post I just can't help but take a few shots at it's treatment of static vs. dynamic languages.
My programming language lineage started by dabbling in C, then taking classes in C++, followed by Java, and finally C#. Most of the real world code I've written has been in static languages, and I've been programming professionally in C# for the last 8 years. This makes me a static language guy.
When I learned ruby, about 5 years ago, I fell in love with it's clean syntax and amazing flexibility. I wrote a few simple tools in it for work, and I've written alot of rspec/capybara tests, plus I dabbled a bit with Rails. I feel I have a decent understanding of the language, but I'm by no means an expert and I definitely still think in classes and types.
I tell you this to explain where I'm coming from. Static languages are what I know best and are what I'm used to. I'm not a dynamic language hater, I'm just comfortable with static langs. Which brings us back to POODR.
POODR talks a lot of about "Duck Types" which are defined in the book as:
Duck types are public interfaces that are not tied to any specific class. These across-class interfaces add enormous flexibility to your application by replacing costly dependencies on class with more forgiving dependencies on messages.I was surprised at this definition because it describes the "Duck type" as being a thing, but in Ruby there is no thing that can represent this across-class-interface. Most treatments of duck typing from Rubyists I've seen usually just talk about how it's a feature of the dynamic nature of the language. They talk about "duck typing" but not "duck types."
In C# we have interfaces, which can be used as explicitly defined duck types. The Dependency Inversion Principle and the Interface Segregation Principle are both trying to get you to use interfaces in this way, instead of just as Header Interfaces. It's good OO because it focuses on messages instead of types. As POODR says, "It's not what an object is that matters, it's what it does."
I think there is a lot of power in Ruby's implicit "duck types," but I also think the lack of explicit interfaces is a serious liability, and I was very entertained by how many hoops POODR jumps through to try to work around this problem, all while trying to claim that it isn't a problem at all, and in fact, it's great!
At the end of Chapter 5, there's a section that tries to convince you that Dynamic typing is better than Static typing. Unfortunately, it just builds up a straw man version of static typing to make it easier to tear down. What it leaves out is interfaces:
Duck typing provides a way out of this trap. It removes the dependencies on class and thus avoids the subsequent type failures. It reveals stable abstractions on which your code can safely depend.If statics langs didn't have interfaces, this might be true. But they do have interfaces! And worse, interfaces represent a significantly more stable abstraction that is dramatically safter to depend on than these invisible "duck types." POODR demonstrates this itself with examples where the "duck type" interface changes, but not all "implementers" of the interface are updated. There's no compiler to catch this. And standard TDD practices wont catch it either. Your tests will be green even though the system doesn't work. So you have to write manual tests that you can share across all the implementers to make sure the message names and parameters stay in sync. Nearly all of Chapter 9 is devoted to testing practices that simply wouldn't be needed if there was even just a rudimentary compiler that could verify just inheritance and interface implementations.
The lack of explicit "duck types" just seems so problematic to me... Keeping them in sync is a chore, and a potential source of error. The worst kind of error too, because the same code may work in one context but break in another based on which "duck type" is used.
Another problem I've run into is when trying to understand some code that takes in a "duck type", how do you figure out the full story of what will happen? How do you find all the implementers of that "duck type"? Just search your code base for one of the method names? Try to find every line of code that injects in a different duck type?
Not being able to surface an explicit interface leaves you stuck in a situation where you have to infer the relationship between your objects by finding every usage of them. Seems like a lot more work, as well as being a recipe for tangled and confusing code.
So what do you think Dynamic language people? Am I making a bigger deal out of the problems of dynamic typing just as Sandi made a bigger deal out of the problems of static typing? Is this just a lack of experience problem? Do you just not run into these issues that often in real world usage?
UPDATE 2/20/2013:
Here's an interesting presentation by Michael Feathers about the power of thinking about types during design. I felt like it had some relevance to the conversation here.
This is very interesting. I had to go back and read that section in the book I learned C# from to understand what you were saying. I've never actually used an interface, though I knew they existed, but they seem to be slightly more practical than what I was picturing them to be (which is probably something akin to the "header interfaces" you mention above). I think I definitely see how static interfaces would be a much safer way to solve this problem: it would seem to avoid problems where an object implements 'to_i()' or '+', but in an unexpected way which subtly breaks something but throws no errors (whereas associating with an interface ensures that those methods must be explicitly defined and thus likely defined as intended). The other thing that seems huge to me, is that it allows someone after the fact to get a sense of what a method does purely by its declaration: "public static Numeric operator +(Numeric a, Numeric b)" probably adds; "public static Text operator +(Text a, Numeric b)" probably concatenates; but what does "def +(a)" do?—and moreso, how does one know what methods it expects 'a' to implement without reading the whole definition as you said?
ReplyDeleteI also went back and read the "Duck Typing" chapter in the book I learned ruby from too (Pickaxe). They explicitly state that their opinion is that, despite there being more potential pitfalls with duck typing, one just doesn't run into these issues often enough in real-world usage to justify the extra overhead of static typing. So that's at least three people's answer to your question. I'll end with their quote at the end of that chapter, because it sounds good:
"Duck typing can generate controversy. Every now and then a thread flares on the mailing lists, or someone blogs for or against the concept. Many of the contributors to these discussions have some fairly extreme position.
Ultimately, though, duck typing isn't a set of rules; it's just a style of programming. Design your programs to balance paranoia and flexibility. If you feel the need to constrain the types of objects that the users of a method pass in, ask yourself why. Try to determine what could go wrong if you were expecting a String and instead get an Array. Sometimes, the difference is crucially important. Often, though, it isn't. Try erring on the more permissive side for a while, and see if bad things happen. If not, perhaps duck typing isn't just for the birds."
I like that last quote, that's a nice balanced way of thinking about it.
DeleteI think part of what I occasionally struggle with in dynamic languages (or think that I might struggle with if I were to work with them more) is that as someone reading existing code I have fewer hints as to what the author intended or was thinking.
In a static language, the method definition gives me:
- parameter type, which immediately leads me to everything that object is capable of
- parameter name, which tells me more about the context the author is writing in
In a dynamic language, I just get the parameter name. And then I have to hunt (a bit) to find what capabilities of that object they are depending on. And if I should want to use a new capability of that object, I may not be able to. And finding that out requires finding every usage of the method to ensure that all the objects being passed in are of the same "duck type."
For example, I may write a method expecting it to receive a String. And if I use only a small subset of it's methods, that method may also work with Array. But if I later update it to use .ToUpperCase, how do I know I haven't broken anyone?
Static typing makes those issues disappear. And good use of interfaces returns some of the benefits of a "duck type." But I'll admit much of this is hypothetical. It's likely these constraints of a dynamic language would simply force you to write code differently. And in many ways, maybe better.
Anyway, thanks for the comment!
Great post. Definitely made me think and clarify my thoughts on this topic. I have a lengthy response in my head but I haven't had time to write it, so I'll give you the abbreviated version for now...
ReplyDeleteThis is not too dissimilar to the Ruby metaprogramming talk I gave at Burning River. My main points were to 1) recognize that the power comes with inherent risks and 2) embrace the language as it exists (don't try to make Ruby into C# or vice versa).
I'm currently working on an open-source statistics gem (https://github.com/jdantonio/ratistics). I intentionally designed the library so that calculations can be performed against virtually any collection object that can be iterated with an #each method. Some functions also require that the collection be accessible by index. You are absolutely correct that the code itself is less expressive than it would be were it written in C# with interfaces. On the other hand, all the functions currently work (and are tested against) Array, ActiveRecord result sets, and most Hamster collection classes, and probably a zillion other collection classes I've never heard of. And I did that with no metaprogramming, monkey-patching or other shenanigans. The flexibility that duck-typing gives me here is unequaled in languages like C# and Java.
I spent the first decade of my career working in static languages (C++, Java, and C#). When I first started heavily using dynamic languages (Python) I was (literally) afraid of dynamic typing. After years of using Ruby as my primary development language I have no interest in returning to statically-typed languages. I don't miss the compiler at all, FWIW.
I think a lot of this comes down to programmer preference. If one approach were undeniably better than the other I don't think we'd have as many language choices.
-Jerry
Thanks Jerry. "I don't miss the compiler at all" I admit this surprises me some, but at the same time, I can see how that would be true. When I play with Ruby, I love not needing to wait for the compiler. Until I typo something... Or want to rename a method or something. Or want to know what methods are available to call on some object. :)
DeleteSounds to me like what you want is some IDE goodness. RubyMine is pretty solid in this regard from what I've heard.
DeleteDo you spend much time in IRB when you use Ruby? Ruby objects have a very rich set of introspection methods. In IRB you can interrogate a module/class/object for its methods, accessors, data members, ancestors, etc. I can learn more about an object in a few seconds in IRB than all the menus in Visual Studio combined. Part of "embracing the language" is learning the tools. Tools like Visual Studio are great in some circumstances (last week I built a Metro app for the Surface Pro and VS 2012 was amazing) but you'll never be truly productive in Ruby if you don't fully embrace REPL-based programming.
ReplyDelete-Jerry