Domanda

I try to write "clean code" for most of the time. But practically find it very hard, meaning - gradually business requirements changes dramatically or the business requirement which seems like just a condition force to modify existing code.

If we go for "extend" instead of "modify" approach, implementation seems clean. But feels like to much class hierarchy for use cases which seems simple. Moreover, sometime just writing few lines of code with "modify" fix the problem in given "Time" instead of writing few set of classes with "extend" approach.

How to ensure to keep code clean not when it is implanted for first time, but few years down the line. Specifically when time is critical and for business new requirements are "JUST a small change".

È stato utile?

Soluzione

Don't follow the Open/Closed principle (OCP) unless you absolutely have to. In most application development OCP is not appropriate and only leads to overly complex code, as you have already noticed.

OCP is appropriate in scenarios where you have independent, external clients using your code, and you need to evolve the code while preserving backwards compatibility. Take the .net framework: MS cannot just modify the behavior of an existing API in the framework, because that would break all the client depending on the current behavior. They can only extend the API. OCP does not actually lead to clean code - for example the frameworks have multiple deprecated API which can never be removed. But it preserves backwards compatibility.

Another scenario is if you have a "big-ball-of-mud" architecture where each change may have unexpected side effects across the system, and no automated testing to protect against regressions. In that case you may adopt a paranoid mindset, and keep the code "closed" to avoid breaking existing code. But here not to modify the code is only treating the symptoms, and will not in itself improve the quality of the code. If possible, you should prefer to treat the root cause of the problems.

If you are not forced to follow the OCP principle then it is often cleaner and simpler to modify behavior rather than extend it.

Like all programming guidelines, OC is not an universal rule but is only appropriate in specific contexts.

Altri suggerimenti

Do you expect OCP to prevent you from changing the code on all business changes? When requirements change from "give me an application that will lend bikes" to "give me an application that will steal cars" no rule will save you.

So why should we follow OCP? Why is it useful? I think Strategy Pattern is the simplest way to explain the power of OCP. Let's consider part of the system that calculates discount for some selling process. Let's assume that discount depends on order value as well as on the user that buys (VIP, regular etc.). One approach is:

public calculateDiscount(Customer customer, Order order) {
    // some common part

    Discount d;

    if(customer.isVip(){
        d = // some code
    } else if(customer.isRegular()){
        d = // some code
    }
    // etc.

    // some common code
}

another one:

public calculateDiscount(Customer customer, Order order) {
    // some common part

    DiscountPolicy dp = customer.getDiscountPolicy();
    Discount d = dp.calculateOn(order);

    // some common code

}

What if there will be another type of Customer? First approach forces you to modify code. Will this change break behaviour of calculateDiscount? Will you be able to make modification without introducing errors? I know, in this simple example probably yes ;)

But there are many parts of your system that you already know or strongly feel that will be modified or extended (not in meaning of inheritance, but in meaning of covering another nuances of use cases) in the future. You have to prevent your code from errors as much as you can. One of the method of such prevention is not changing the code that works. OCP is all about not changing code that works. Strategy, Decorator, Abstract Factory, Command etc. patterns are all about keeping some part of code in compliance with OCP.

Obviously, there are changes that you are not prevented from. Adding requirement to calculate discount depending on market type also will end up in changing contract and code inside calculateDiscount method as well as unit tests code and other tests that you have. It is inevitable. But OCP gives you the "crumple zone". Like in real world - given crumple zone is not always sufficient for crashes we come across.

Following the OCP should always be one of the goals of an OOP programmer. Ignoring this principle erodes how useful it is to decompose into objects. It is exceptionally lazy to think, "Well if they want to react to change they can just rewrite it".

OCP asks you to favor a design that permits change by adding new code rather than changing old code.

The structural mechanisms for this can as complex as a design pattern or as simple as introducing a variable. Abstraction works best when you can't see the details you're using.

The most telling problem is when you see fit to separate one idea from another by putting them in different objects yet one KNOWS exactly which implementation of the other it's talking to. Doing this not only violates OCP but it means you typed up an extra class for no good reason. If you separate ideas into different objects they should not hold each other in a death grip.

However, change is difficult to predict. Yagni teaches us not to create things that might be useful, only things that are useful today.

Rather then predict the future, be conservative when you assume something will be stable. Our high level abstractions, our interfaces, really hurt us when they change. Keep what they assume to be stable to a minimum. Push what you're not sure of down to lower levels. Break them up to keep their vulnerability to change footprint small. Let them serve only one master.

Do this, and following OCP shouldn't be too dramatically different. If, however, you go nuts slapping interfaces on every object and refuse to call anything that isn't polymorphic, well you're giving us OOP coders a bad name.

The best advice you could apply here was actually about when to react to a DRY violation. You are far more likely to decompose correctly the second time you repeat yourself then you are the first time. So maybe don't be so quick to react.

This wisdom should temper how quick you are to predict change. Unit tests help us see how we could change implementation details. While this is a good structural exercise unit tests aren't production code. Be careful thinking they show you how things will change.

But you should feel bad every time you add a feature or fix a bug by changing existing code. Even if you don't actually have independent external clients it is really nice when you can support large swaths of your code as if it were.

Remember, these principles are not going to help you get your code to work any faster. They help you keep your code working once it does. Therefor you wont find out if you're wasting your time putting effort into following them until changes start coming in. If you want to practice applying them then you can't just code to a fixed specification. You have to code to a changing specification that surprises you.

A conservative way to apply these principles is to add complexity in reaction to change rather than in anticipation. When I take this tract I'll forgive myself for having to change existing code in a module once, maybe twice. After that it's time to see about stopping that from happening again.

Otherwise we might as well go back to procedural programming because even it can deal with change if you're willing to rewrite every time.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
scroll top