BrainsToBytes

Depend on behavior, not data

Depend on behavior, not on data

This is one of the most powerful pieces of advice in OO programming I've ever received.

It means that objects shouldn't be seen just as a collection of data structures. Instead, you should visualize them as software entities that can respond to messages and answer questions about themselves.

We achieve this by hiding implementation details and only exposing a well-defined public interfaces. The parts of your program using an object don't need to know about the internal workings of the object, they just need to know how to use the object's interface. This helps you create modular designs that are easier to extend and maintain.

Information hiding is a powerful technique used to achieve this goal: it prevents other pieces of code from accessing implementation details of the class. This is one of the concepts that took me the longest to appreciate. In the beginning, I didn't really understand what's the point of private methods and attributes. After understanding the principles behind OO programming, I understood that it's all about managing complexity.

Hiding the details of pizza&pineapple

The parts of your program don't need to know all the details of the classes they use. Usually, all they need to know is which method to call and which arguments to pass. It's much easier to visualize with an analogy:

Imagine you go to a restaurant to eat pizza. You are used to a very simple interface: a waiter takes our order and then we received the pizza after some minutes. Now imagine that the waiter, instead of taking your order, leads you in the kitchen and asks you to guide the chef, step-by-step, in the pizza preparation process.

This restaurant's inconvenient interface is the result of not using information hiding. You, as the customer, do not care (or even know) how to prepare a pizza, that's the chef's responsibility. All you need to provide is the name of the pizza type you want and voila! Instead of using this interface:

    order_pizza(:hawaiian)
    # OR
    order_pizza(:hungarese)

You ended up specifying the whole process:

    chef.ask_to_prepare_dough(:thin_dough)
    chef.ask_to_add_ingredient(:ham)
    chef.ask_to_add_ingredient(:cheese)
    chef.ask_to_add_ingredient(:chilli)
    chef.ask_to_add_ingredient(:pepperoni)
    chef.ask_to_put_pizza_in_the_oven(:toasty)
    chef.ask_for_final_pizza

This results in a tightly coupled piece of code. Any change in the details of our chef class will heavily impact all the code that uses it. For this reason, all the details and data used in complex procedures must be kept hidden within our objects. This also applies to the data structures our objects use.

Hi, I'm Robobob and my serial number is XAWE-15915975357

You should not access the attributes of an object directly.

Suppose you have a Robot class and you want to access some information about our artificial buddy (the variable that holds the serial number), you can write the following line for accessing the serial number:

    print "This is my robot's serial number: #{robbierob.serial_number}"
    #...
    #... serial_number is an instance variable
    robbierob.serial_number = "NAVE-48622684268

This code has at least 2 problems:

1- What if the serial_number variable changes in format or type? you would need to go through all the code that referenced the instance variables and change it so that it works with the new change.

2- A more subtle issue is that you are seeing your robot class as a glorified data structure, not like an object that can respond to messages (methods) and give you information about itself or alter its state.

The solution is to, instead of depending on the data (the attribute itself), you can depend on behavior (functions), that’s the reason we use ‘setters’ and ‘getters’ to access attributes. We declare serial_number as private, that way we are hiding the data behind a facade of behavior.

Adding setters and getters solves the first problem listed above: If for some reason you need to make a change in the details, just change those two functions. The rest of the code won't even notice something is different as long as you keep honoring the interface.

Sadly, just adding a layer of setters and getters doesn't solve the second problem. Depending on behavior, not on data, is more than just adding a layer of methods in front of your data, it's about creating proper abstractions.

Enforcing an access policy through abstraction

As we just mentioned, just a couple of methods is not enough. We want to have objects that respond to meaningful messages, not just glorified data structures hiding behind mutators and accessors.

Suppose all robots have a line name and a series number. The lines names are 4 uppercase characters (like "XAWE" and "NAVE"), while the series number is a numeric value with 11 digits. From requirements, you know that it's useful to set these two values individually and that users only need to get the full identifier in the format LLLL-ssssssssss.

You can enforce this policy by creating two methods for setting those values individually, and one for getting the full identifier. You could use a robot object in this way:

    robbierob.set_product_line("SUHR")
    robbierob.set_serial(15975464682)
    #...
    print "Our robot's complete serial number is #{robbierob.get_identifier}"

How are the product line and serial number stored inside robot objects? Do they have one variable for each value? are they aggregated in a single string? is there a special struct or even an identifier object?

We don't know, and we don't care. All we need to know is the interface: which methods are there and what are the requirements for calling them. Note that the implementers of Robot decided that you can't get the individual values that compose the identifier. This form of control access is common, as is the opposite: forbidding you from setting individual values but being able to query them individually.

This is a more 'objecty' approach. Remember that the power of OO programming lies in modeling reality through packages of behavior or objects. If you build the right abstractions, you can create something that is more than a glorified data structure.

This, of course, doesn't apply to data transfer objects.

The Law of Demeter

Finally, let's take a look at a useful heuristic for measuring the level of coupling in your code, the Law of Demeter (LoD). This is a good indicator of the quality of your abstractions and how hidden your implementation details are.

It tells you that an object shouldn't call methods on objects that aren't close to it. You can measure the 'closeness' of an object by counting the amount of dots ('.') in the method chain. The following is an example of calling a method far way from the caller:

    spaceship.crew.get_captain().inventory.get_tool(:telephone).send_message(:mom, "I'm fine mom, say hi to dad")

This previous example is a violation of the LoD, as spaceship is calling a method (send_message) in an object very 'far' from itself. Method calls with lots of dots in the chain are usually called trainwrecks.

Ok, but how many dots are allowed?

The Law of Demeter is not a hard rule, it's a heuristic. If you have long method calls, it's likely that you have problems with the definition of your interfaces. If objects need functionality from other objects that live too far, you might want to restructure your program so that they are closer. The LoD is an indicator of possible trouble with the way your objects are organized in your program.

Bob Martin regards as valid the following calls in a function F of object O:

  • Other methods in O.
  • Methods in objects created inside of F.
  • Methods defined in objects passed into of F as arguments.
  • Methods defined in objects held in instance variables of O.

Anything beyond this is a violation of the LoD and might mean that your code needs restructuring.

The difference is in the details

This might seem like a very small thing, but my experience has taught me that in this profession, details make all the difference.

The difference between easy to maintain code and legacy nightmares lays in how effectively you can decouple your code. Modular designs with cohesive and decoupled classes are a joy to work with, and this idea puts you a step closer to that goal.

What to do next:

  • Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
  • This and other very helpful books can be found in the recommended reading list.
  • Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!
Author image
Budapest, Hungary
Hey there, I'm Juan. A programmer currently living in Budapest. I believe in well-engineered solutions, clean code and sharing knowledge. Thanks for reading, I hope you find my articles useful!