Practical Object-Oriented Design in Ruby
I got recommended to this book by a group of developers that I highly admire. I think they were politely telling me that it will help my code if I read it. For a long time I have been writing OO code, however have I been writing it correctly? Turns out like many of you I believed that I was writing code in a OO way. Turns out that
good OO is very rare.
This book takes a very pragmatic approach on the subject. Many of the concepts you would have heard, however the way they are described makes them seem so obvious. That is when I know it is going to be a good book.
Designing Classes with Single Responsibility
The book talks about 4 properties that will make your code easy to change:
- Transparent - The consequence of change should be obvious in the code that is changing and in distant code that relies upon it.
- Reasonable - The cost of any change should be proportional to the benefits the change achieves.
- Usable - Existing code should be usable in the new and unexpected contexts.
- Exemplary - The code itself should encourage those who change it to perpetuate these qualities.
Creating code that is TRUE will guarantee that your class has a single responsibility.
Why does this matter?
A class that has more than one responsibility is harder to reuse.
How can we determine if a class has single responsibility?
A way to find if the class should define the behaviour is to interrogate it. Ask your self "Please Mr/Mrs Class what is your behaviour?" If the question sounds odd, more than likely it is the wrong class.
We can also try to describe what the class does. Pay attention to words like "and/or" these words are a giveaway that your class is doing too much. This applies to messages as well.
Writing Code That Embraces Change
- Hide instance variables in a method - Don't use instance variables throughout your class.
- Hide data structures - As data changes depending on data structures can become leaky. It is best to make sure that we return an object with well defined interface. In ruby we can use the Struct class.
- Extract Extra responsibilities from Methods - For example if you are performing something in a loop, more than likely what is in the loop could be a method (or a few). Make sure move the smallest responsibility to a method.
- Isolate extra responsibilities to classes - Have a look at your class and interrogate it. Chances are that some of your methods don't belong in that class.
Managing Dependencies
Well designed objects have a single responsibility, their very nature requires that they collaborate to accomplish complex tasks. To collaborate, an object must know about other objects. This knowing is what creates dependencies. Here is where we can get into trouble as some of these dependencies can strangle out application.
Understanding Dependencies
An object depends on another object if, when another object changes, the other might be forced to change in turn.
How can we recognise dependencies:
- A name of another class is in your class
- Your class sends messages to other objects.
- The arguments that a message requires and the order.
Dependencies are inevitable. However we can keep them under control.
Writing Loosely Couple Code
Every dependency is like a little dot of glue that causes your class to stick to the things it touches. How can we write loosely couple code?
Inject Dependencies
Having a name of a class causes a dependency in your class. By including a name of a class we introduce a static type in our class. The beauty of Ruby is that we don't need to depend on a type. What we want is to depend on a message. By injecting a dependency all we care about is the message the object can respond to.
Isolate Dependencies
Think of every dependency as an alien bacterium that's trying to infect your class. Give your class a vigorous immune system; quarantine every dependency.
How do we accomplish this?
- Isolate instance creation. Move the creation to its own method using ||= operator.
- Isolate vulnerable external messages. It is best to encapsulate an external message in its own method. If anything changes you isolate the change.
Remove Argument-Order Dependencies
Knowing an object or message parameters and their order causes a dependency. It is best to try to avoid this order. In Ruby this can be achieved by the
following. It is important to make sure that we provide sensible defaults. This can be achieved by using the
following.
If you have a class that you can't control. It is best to create a small adapter class that makes sure there is no order dependency.
Creating Flexible Interfaces
Design is concerned with the messages that pass between objects. The conversation between objects takes place using interfaces.
Starting is hard, this is why TDD is hard to get right. The important thing to remember is to not focus so much on the domain objects, however focus on the messages. This means that rather than focus on the class focus on the behaviour. These messages are guides that lead you to discover other objects, ones that are just as necessary but far lest obvious.
How can we discover discover these messages:
- Use sequence diagrams. These diagrams provide a simple and lightweight way to experiment with different object arrangements and message passing schemes. This allows us to try a few things before committing to them.
- Asking for "what" instead of telling the "how". There is a big distinction between a message that asks for what the sender wants and a message that tells the receiver how to behave.
This is where I have found TDD to fail me as I did not put much thought into what the interface should look like. It is important to put some upfront analysis (thought). This helps us create a message-based application, which is the essence of an OO application
Conclusion
These are the messages from the book that I have found to be the most interesting. There are more however I didn't want to spoil it all. Some of the other topics I think warrant a blog post of their own. I urge you to get a copy of this awesome book.