Locality and Cohesion

"The primary feature for easy maintenance is locality: Locality is that characteristic of source code that enables a programmer to understand that source by looking at only a small portion of it." -- Richard Gabriel

This advice is from Patterns of Software by Richard Gabriel.

Keeping cohesive parts of our system together can help us understand it. By managing locality we can keep cohesive parts together.

It’s easy to see coupling in our code. When one object can't do it's job without another, we experience frustration in the face of change. We often think about dependencies in our code, but cohesion is the relatedness of the behaviors and plays an import part in how we organize the ideas to support our domain.

def process_payment(amount)
      gateway.authorize_and_charge(amount) do
        deliver_cart
      end
      logger.info "handling payment: #{amount}"
      logger.info "cart delivered: #{id}"
    end

The exact purpose of this completely-made-up code isn't that important. But we can look at parts of this procedure and extract them into a related method:

def process_payment(amount)
      gateway.authorize_and_charge(amount) do
        deliver_cart
      end
      log_purchase(amount)
    end

    def log_purchase(amount)
      logger.info "handling payment: #{amount}"
      logger.info "cart delivered: #{id}"
    end

As Gabriel points out in his book, we can compress a procedure into a simple phrase like log_purchase but this compression carries a cost. In order to understand the behavior of this log_purchase phrase, we need to understand the context around it.

Indeed, we might look at this and realize that there's a problem with the way we managed the locality of the procedure. Instead of easily understanding a single method, we might look at process_payment and realize there's a bit more to it than we first expect.

We're forced to understand the log_purchase and the context which previously surrounded it's procedure. A second look at this extraction might lead us to reconsider and to go back to inline the method. Let's keep this code with a tighter locality:

def process_payment(amount)
      gateway.authorize_and_charge(amount) do
        deliver_cart
      end
      logger.info "handling payment: #{amount}"
      logger.info "cart delivered: #{id}"
    end

While extracting the log_purchase method was easy, given the original code, it added a bit too much for us to understand and it doesn't feel quite right. Handling the locality of this code helps us to better understand it and to make better decisions about how to improve the main process_payment method.

Consider this: How much must you pack into your head before you can begin evaluating a part of your code?

While breaking procedures up into small methods can be a useful way to make easy to understand (and easy to test) parts, we may do so to the detriment of understanding.

This is something to consider if you are building a DSL to compress ideas in your code or if you're trying to create objects to manage your business logic. I'll be writing more about the value of controlling the locality of behavior in your system, but I'd love to hear how you manage locality. What do you do to ensure that related bits stay together?

Get a FREE sample chapter

Get a sample chapter of Clean Ruby and you'll be added to my periodic newsletter of helpful Ruby tips.