Skip to content

Instantly share code, notes, and snippets.

@brianstorti
Created October 5, 2012 12:58
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save brianstorti/3839690 to your computer and use it in GitHub Desktop.
Save brianstorti/3839690 to your computer and use it in GitHub Desktop.
Practical Object Oriented Design in Ruby

#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 ObejctB 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment