Skip to content

Practical Object Oriented Design in Ruby

  • Design that anticipate specific future requirements almost always end badly.
  • Practical design does not anticipate what will happen to your application, it merely accepts that something
    will and that, in the present, you cannot know what. It does not guess the future; it preserves your options for accommodating the future.
  • It doesn't choose; it leaves you room to move.
  • The purpose of design it to allow you to do it later and its primary goal is to reduce the cost of change.

Design is more the art of preserving changeability than it is the act of achieving perfection.

Code should be TRUE:
Transparent - The consequences of change should be obvious in the code that is changing and in distant code 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 new and unexpected contexts.
Exemplary - The code itself should encourage those who change it to perpetuate these qualities.

Techniques that you can use to create code that embraces change

Depend on Behavior, Not Data

When you create classes that have a single responsibility, every tiny bit of behavior lives in one and only one place.

The phrase "Don’t Repeat Yourself" (DRY) is a shortcut for this idea. DRY code tolerates change because any change in behavior can be made by changing code in just one place.

Hide Instance Variables

Always wrap instance variables in accessor methods instead of directly referring to variables.

def var
  @var
end
Implementing this method changes var from data (which is referenced all over) to behavior (which is defined once).

If the @var instance variable is referred to ten times and it suddenly needs to be adjusted, the code will need many changes. However, if @var is wrapped in a method, you can change what cog means by implementing your own version of the method, like:

def var
  @var * unanticipated_adjustment_factor
end

Hide Data Structures

If being attached to an instance variable is bad, depending on a complicated data structure is worse.
For instance:

def calculate(data)
  data.collect {|cell| cell[0] + (cell[1] * 2)}
end
To do anything useful, each sender of data must have complete knowledge of what piece of data is at which index in the array.

The calculate method not only know how to calculate (whatever it's calculating), but also knows the internal structure of the data array. It depends upon the array's structure. If that structure changes, then this code must change. It's not DRY. The knowledge that something are at [0] should not be duplicated, it should be known in just one place.

Enforce Single Responsibility Everywhere

Creating classes with a single responsibility has important implications for design, but the idea of single responsibility can be usefully employed in many other parts of your code.

Extract Extra Responsibilities from Methods

Methods, like classes, should have a single responsibility. All of the same reasons apply.
Separating iteration from the action that’s being performed on each element is a common case of multiple responsibility that is easy to recognize:

def diameters
  wheels.collect {|wheel| wheel.rim + (wheel.tire * 2)}
end

This method clearly has two responsibilities: it iterates over the wheels and it calculates the diameter of each wheel.

This could be refactored to:

# first - iterate over the array
def diameters
  wheels.collect {|wheel| diameter(wheel)}
end

# second - calculate diameter of ONE wheel
def diameter(wheel)
  wheel.rim + (wheel.tire * 2))
end

The impact of a single refactoring like this is small, but the cumulative effect of this coding style is huge. Methods that have a single responsibility confer the following benefits: * Expose previously hidden qualities: Makes the class' purpose clear, and sometimes reveals wrong responsibilities
Avoid the need for comments: Comments get out of date, turn comments into methods names
Encourage reuse: It's easier for other programmers to reuse the method, instead of duplicating the code
Are easy to move to another class*: When you get more design information and decide to make changes, small methods are easy to move

Isolate Extra Responsibilities in Classes

Once every method has a single responsibility, the scope of your class will be more apparent and some methods will start "feeling wrong". Extract them to its own class it it's possible, but take care: Any decision you make in advance of an explicit requirement is just a guess.

Don’t decide, preserve your ability to make a decision later.

Focus on the primary class, decide on its responsibilities. If you identify extra responsibilities that you cannot yet remove, isolate them. Do not allow extra responsibilities to leak into your class.

Managing Dependencies

Because well designed objects have a single responsibility, their very nature requires that they collaborate to accomplish complex tasks. This collaboration is powerful and perilous. To collaborate, an object must know something know about others. Knowing creates a dependency. If not
managed carefully, these dependencies will strangle your application.

Every dependency is like a little dot of glue that causes your class to stick to the things it touches. A few dots are necessary, but apply too much glue and your application will harden into a solid block. Reducing dependencies means recognizing and removing the ones you don’t need.

Coupling Between Objects (CBO)

Each coupling creates a dependency. The more ObjectA knows about ObjectB, the more tightly coupled they are. The more tightly coupled two objects are, the more they behave like a single entity. If you make a change to ObjectB you may find it necessary to make a change to ObjectA. If you want to reuse one of these, the other comes along. When you test one, you’ll be testing the other too.

It's ok to one to depends upon another, the problem is when all of these dependencies act like one thing, they can be moved alone. In other words, when they are not managed, the entire application can turn into an entangled mess.

Inject dependencies

When ObjectA hardcodes a reference to ObjectB, it is explicitly declaring that it is going to work with this specific type of objects.
The ObjectA refuses to work with other kind of objects that could work as well. For instance:

#No animal was hurt while this code was written
class TailPuller
  def pull
    dog = Dog.new
    dog.pull_tail
  end
end
The TailPullerclass has one responsibility: pull the dog's tail. But why I'm restricting it to pull only Dogs tail when it could be used with any animal that have a tail? I don't wanna to write another class just to pull the Cat's tail. I should've injected this dependency in order to have a more reusable code:
class TailPuller
  def pull(animal)
    animal.pull_tail
  end
end
Of course this is an over-simplistic example, but the idea is quite simple: It is not the class of the object that’s important, it’s the message you plan to send to it.

TailPuller needs access to an object that can respond to pull_tail, a duck type. TailPuller does not care and should not know about the class of that object.

Isolating dependencies

When working on an existing application you may find yourself under severe constraints about how much you can actually change. If prevented from achieving perfection, your goals should switch to improving the overall situation by leaving the code better than you found it.

If you cannot remove unnecessary dependencies, you should isolate them within your class. When you have a code like this:

class Gear
  attr_reader :chainring, :cog, :rim, :tire

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @wheel = Wheel.new(rim, tire)
  end

  def gear_inches
    ratio * wheel.diameter
  end

You clearly have an undesired Wheel dependency, that doesn't make much sense, sou you can isolate it:

class Gear
  attr_reader :chainring, :cog, :rim, :tire

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @rim = rim
    @tire = tire
  end

  def gear_inches
    ratio * wheel.diameter
  end

  def wheel
    @wheel || Wheel.new(rim, tire)
  end
In both of these examples Gear still knows far too much; it still takes rim and tire as initialization arguments and it still creates its own new instance of Wheel. Gear is still stuck to Wheel; it can calculate the gear inches of no other kind of object. However, an improvement has been made. These coding styles reduce the number of dependencies in gear_inches while publicly exposing Gear’s dependency on Wheel. They reveal dependencies instead of concealing them, lowering the barriers to reuse and making the code easier to refactor when circumstances allow.

Managing Dependency Direction

Dependencies always have a direction, and these direction usually can be reverted. When A have a B dependency, I can remove it and make B depends upon A.

But how to decide the best direction for the dependency?

Pretend for a moment that your classes are people. If you were to give them advice about how to behave you would tell them to depend on things that change less
often than you do.

This short statement belies the sophistication of the idea, which is based on three simple truths about code:
* Some classes are more likely than others to have changes in requirements. * Concrete classes are more likely to change than abstract classes. * Changing a class that has many dependents will result in widespread consequences.

Summarizing, the rule of thumb would be: Depend on things that are less likely to change.

Depending on an abstraction is always safer than depending on a concretion. We already made it when we talked about dependency injection. The dependency of an
animal instance (or, in other words, depending on the interface pull_tail of the received object) is way safer than the dependency of a concrete Dog.

Summary

Dependency management is core to creating future-proof applications. Injecting dependencies creates loosely coupled objects that can be reused in novel ways.
Isolating dependencies allows objects to quickly adapt to unexpected changes. Depending on abstractions decreases the likelihood of facing these changes.
The key to managing dependencies is to control their direction.

Creating Flexible Interfaces

It’s tempting to think of object-oriented applications as being the sum of their classes. There is design detail that must be captured at this level but an object-oriented application is more than just classes. It is made up of classes but defined by messages.

Design, therefore, must be concerned with the messages that pass between objects. It deals not only with what objects know (their responsibilities) and who they know (their dependencies), but how they talk to one another. The conversation be- tween objects takes place using their interfaces.

Source

More