DEV Community

Derick Rentz
Derick Rentz

Posted on

Preventing Broken Window Theory using Design Patterns

The broken window theory, introduced by Andrew Hunt and David Thomas in The Pragmatic Programmer: From Journeyman to Master, is one of the most important concepts in software development. The authors suggested that things, such a system, suffer of something called entropy. Entropy, as a physics term, is translated as the disorder level in a system. Unfortunately, this level is always tending to the maximum, which means that, if there is entropy, there will be more and more entropy.

In 1982, James Q. Wilson and George L. Kelling in their article, "Broken windows", gave the following example: Imagine a building with some (few) broken windows. If the windows are not going to be repaired as soon as possible, the tendency is that someone else (vandals, e.g.) will break more windows, and, eventually, break into the building, light a bonfire inside it, etc.

But, in 1969, Philip Zimbardo, a Stanford psychology professor, tested the theory in a practical way. He left a car without licence plates in a Bronx neighbourhood, and a second car, with the same conditions, in Palo Alto. The car that was in Bronx, was attacked within minutes after its abandonment. After less than 24 hours, the vandals had stripped everything of value from the car. Some time after, the windows of the car were smashed in, the upholstery was ripped, and children were using the car as playground. Meanwhile, the car in Palo Alto stayed untouched. After one week without anyone doing anything to the car, Zimbardo himself smashed it with a sledgehammer. Very soon after that, the same thing of the other car started to happen. People started to destroy the vehicle as well. Zimbardo noticed that, in both cases, the individuals who attacked the car were primarily well-dressed, clean-cut and seemingly respectable individuals.

Now you might be thinking: "Okay, how can the 'windows' of the software that I am developing get broken?". Well, similarly to Zimbardo experience, if there is a piece of code with bad quality, the tendency is that there will be more bad quality code. Remember, if there is entropy, there will be more entropy.

So, lets imagine the following: you are a developer, that has the task of develop a feature for your system which calculates taxes for the user, depending on its type. For some reason you need to deliver the task in only some hours, so you final code looks something like this:

public class User {

    private String userType;

    public void calculateTaxes() {
        if (userType.equals("A")) {
            //do A
        } else if (userType.equals("B")) {
            //do B
        } else if (userType.equals("C")) {
            //do C
        } else {
            //do D
        }
    }
}

Will this code work? Yes, absolutely. It will be perfect fine for your necessity of delivering it in a short time. But, there is a huge broken window. It is inflicting a series of rules from clean code: Why is the User class calculating its own taxes? The user should not do that. There is a immense responsibility problem. The second big problem is: If, in another moment, someone else wants to do calculation of tax for the type of user C, what will the developer do? Probably add another else if condition, or, at the best of our bets, change the code to a switch case. And the code will increase and increase. And why? Because you started the bad code. You broke the first window, and left it there without fixing it. If there is entropy, there will be more entropy.

So, considering that you broke the window, but you want to fix it as soon as you can, so the other windows won't be broken. What can be done?

The first problem that could be noticed is that User class is doing things that should not. Considering that User is a model, something that potentially is stored in the database of the application, it should not contain any 'advanced' logic. Lets solve it then, by creating a class which do services for the User.

public class UserServiceImpl implements UserService {

    @Override
    public void calculateTaxesForUser(User user) {
        if (user.getUserType().equals("A")) {
            //do A
        } else if (user.getUserType().equals("B")) {
            //do B
        } else if (user.getUserType().equals("C")) {
            //do C
        } else {
            //do D
        }
    }

}

Are all the problems solved? Absolutely not. There is still a problem of responsibility. If we wanted to calculate tax for another thing, not only user, we would need to create another class which does so. So why not put all the taxes calculation logic in a central class, responsible only for calculation of taxes?

public class TaxesCalculationServiceImpl implements TaxesCalculationService {

    @Override
    public void calculateTaxesForUser(User user) {
        if (user.getUserType().equals("A")) {
            //do A
        } else if (user.getUserType().equals("B")) {
            //do B
        } else if (user.getUserType().equals("C")) {
            //do C
        } else {
            //do D
        }
    }

}

Now we can say that we solved the first problem: responsibility. The User classes do not calculate their own taxes. We could, of course, call the method that calculates the taxes from the UserService, if we want to manipulate the result, but we can not do the calculation there. Now, with this improvement, our code is getting better, our window is not that broken any more. Now, if someone else wants to extend the functionality of the tax calculation method, at least will do in the right place. Or in a place which is not that wrong.

But, the code is far from perfection. There is still a huge problem: The code is fostering the addition of another if statement in order to increase its options of users. How can this be solved? The Strategy Design Pattern (or function pointer) has the answer.

Design Patterns were thought to facilitate the development of the code. Of course, sometimes they can be not that easy to implement, but in most cases, it is worth it.

The Strategy Design Pattern suggests the following: if there is a class which does a specific thing in various different ways, then these ways should be splited in different classes, which are called strategies, having a common interface that is implemented. This is exactly our problem. The method which does the calculation, does it in a different way for each type of user. So, the solution will be to split each of this ways in different classes:

public class TaxCalculationStrategyForUserTypeA implements TaxCalculationStrategy {

    @Override
    public void calculateTaxesForUser(User user) {
        //do A
    }
}
public class TaxCalculationStrategyForUserTypeB implements TaxCalculationStrategy {

    @Override
    public void calculateTaxesForUser(User user) {
        //do B
    }
}
public class TaxCalculationStrategyForUserTypeC implements TaxCalculationStrategy {

    @Override
    public void calculateTaxesForUser(User user) {
        //do C
    }
}
public class TaxCalculationStrategyForUserTypeD implements TaxCalculationStrategy {

    @Override
    public void calculateTaxesForUser(User user) {
        //do D
    }
}

Now that we have all the strategies for our type of users, the service will look like this:


public class TaxesCalculationServiceImpl implements TaxesCalculationService {

    @Resource
    private TaxCalculationStrategy taxCalculationStrategy;

    @Override
    public void calculateTaxesForUser(User user) {
        taxCalculationStrategy.calculateTaxesForUser(user);
    }

    @Override
    public void setTaxCalculationStrategy(TaxCalculationStrategy taxCalculationStrategy) {
        this.taxCalculationStrategy = taxCalculationStrategy;
    }

}

In this way is possible to change the strategy used by the service at runtime, whenever is necessary:

public class Application {

    @Resource
    private TaxesCalculationService taxesCalculationService;

    public void doTaxCalculationForUser (User user){
        taxesCalculationService.setTaxCalculationStrategy(new TaxCalculationStrategyForUserTypeC());
        taxesCalculationService.calculateTaxesForUser(user);
        taxesCalculationService.setTaxCalculationStrategy(new TaxCalculationStrategyForUserTypeB());
        taxesCalculationService.calculateTaxesForUser(user);
    }

}

Of course, each application will implement it in its own way.

You might be thinking: "Polymorphism could have solved this problem too". Yes, that's true. Maybe won't be the best option, but would have solved, indeed. But, the point is that our code now is better. We have fixed the broken window that had been left behind. Probably who will extend the functionalities will check how is the implementation and follow the pattern, keeping the code clean.

The broken windows should be a constant concern on software development. Refuse yourself to keep developing on top of bad code. Fix the broken windows first, look for the best approaches, learn new things and prevent the new ones to appear. This will not only make yourself a better developer in technical terms, but will improve the quality of your development journey as a whole.

There is no entropy, so there will be no entropy.

Top comments (0)