4

O/C principle dictates that production code should not be changed if system behavior augmention is required. Otherwise it becomes hard to trust tests.

Following it as a dogma we are going to end with either deep inheritance trees or aggregations.

On the other hand we never tried this approach so the former statement is more of an assumption. We ignore O/C principle completely what expectedly brings us a lot of pain. But augmention frequently comes along with interface changes so the design gets 'shaked' from iteration to iteration and I hardly imagine how it is even possible to adhere to O/C principle.

Shouldn't some notion of feature completeness exist only after achieving which code changes should be discouraged?

2
  • My understanding of O/C is that if you need new behaviour you add new classes or new methods to existing classes. Existing methods in existing classes should not change their behaviour. Once a class has become mature and stable enough and a lot of code depends on it, I do not see why this should be a problem.
    – Giorgio
    Commented Dec 16, 2014 at 12:07
  • Did you read blog.8thlight.com/uncle-bob/2014/05/12/…
    – Doc Brown
    Commented Dec 16, 2014 at 13:33

2 Answers 2

17

The open/closed principle is a tricky one, and I think it's often misunderstood. In the strict sense, obviously it's impossible to add behaviour to an application without modifying its code, but what we really mean by open/closed is that it should be possible to extend the functionality of a given class without changing that class. Let me try to come up with a simple contrived example. Let's say you have an Order class, which has a price method that sums the prices of all the items in the order:

class Order
  def price
    items.inject(0) { |total, item| total += item.price }
  end
end

Now, say you have a new requirement that some orders will be taxable (for customers in the same state, e.g.) and others won't be. Regardless of whether you want to adhere to O/C, you're going to have to change the Order class. That's just inevitable. But how do you do it? The obvious approach is a conditional:

class Order
  def price
    base_price = items.inject(0) { |total, item| total += item.price }
    if taxable?
      base_price * 1.1
    else
      base_price
    end
  end
end

However, the thing to realise here is that the next time someone requests a change to how order prices are calculated, you will still have to make another change to this class. And as those changes stack on top of each other things get out of hand. So what open/closed says is that you should refactor the existing functionality in such a way that it becomes possible to make the change without altering Order.

There are a million ways of doing this, but for the sake of argument let's say you want to employ a sort of strategy pattern: your Order depends on one of many PriceCalculator subclasses to compute the price, and the calculator is injected when you instantiate an Order. Now, when you inevitably get the request to add a shipping fee to some orders (but not others), you can swap out the PriceCalculator with a PriceCalculatorWithShipping (or whatever), and not have to touch Order.

So to sum up, I'd say open/closed isn't discouraging code changes, it's just trying to make sure those changes happen in as few places as possible, and that you have fewer reasons to make extensive modifications to existing classes. I'd argue that in the medium term this makes applications much easier to change, and so is entirely compatible with the agile philosophy.

For a really great longer-form exposition of these ideas I'd recommend this talk by Sandi Metz: https://www.youtube.com/watch?v=8bZh5LMaSmE.

1
1

"O/C principle dictates that production code should not be changed if system behavior augmention is required."

My understanding of O/C was not that a programmer should not modify a class over time (there's no fundamental risk to tests as we can and should always update our tests alongside any changes to functionality), but that classes shouldn't expose enough of their internal workings that other code can effectively modify their behaviour. That is, if I create a class Foo with a complete set of unit tests, it should not be possible to create a class DerivedFoo such that if I use it in a Foo test, that test will fail.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.