Forbid Indecisive Objects

Completely get rid of objects that behave like two (or more) objects.

There's a special kind of object I tend to find still too often in object-oriented codebase. I call these objects Indecisive Objects.

These objects have a very discernable shape. Take this one for example.

class DateRange {
    private start: Date;
    private end: Date;
    private inBusinessDays: boolean;
    
    isInRange(date: Date): boolean {
        if (this.inBusinessDays) {
            // One implementation that doesn't take week-ends into account
        } else {
            // Another implementation that do that
        }
    }
    
    addDaysToEnd(days: number) {
        if (this.inBusinessDays) {
            // skip saturday/sunday
        } else {
            // take saturday/sunday into account
        }
    }
}

In their simplest, most recognisable form, the Indecisive Objects use a boolean to pivot between one or another implementation. They act as one object if that boolean is true, and as another object when that boolean is false.

To refactor these one in proper objects, you only have to use that boolean pivot and use it as a hint to name your new objects.

class BusinessDateRange {
    private start: Date;
    private end: Date;
    
    isInRange(date: Date): boolean {}
    
    addDaysToEnd(days: number) {}
}

class DateRange { 
    private start: Date;
    private end: Date;
    
    isInRange(date: Date): boolean {}
    
    addDaysToEnd(days: number) {}
}

Keep in mind that the distinction is made on the behavior. Indecisive objects are two objects that perform different behavior in different states. Two objects with different states but no fundamental behavior (data-structures, really) are not concerned.

But Indecisive Objects also come in much more subtle flavours that can be very difficult to spot.

class Tax {
    getTotal() {
        if (this.year === 2024 && this.isCompany) {
            // Some result
        } else if (this.year === 2023 && this.isCitizen) {
            // Some other result
        } else if (this.year === 2022 && this.isCompany) {
            // Yet another calculation
        }
        // Some default implementation
    }
}

Such is the most common form of Indecisive Objects that plague our codebases. Not only they're hard to recognise but they're also hard to refactor correctly.

One possibility would be to take each condition and factor them out inside distinct classes. But it doesn't yield the best results. Very often, the recognition of this pattern should yield additional questioning and refinement of the domain model. What really is the meaning of these conditions ?

In this context, it often helps to simply reorganize the code a little bit and tinker with it.

class Tax {
    getTotal() {
        if (this.isCompany) {
            if (this.year === 2024) {
                // Some code
            } else if (this.year === 2022) {
                // Some other code
            }
        } else if (this.isCitizen) {
            if (this.year === 2023) {
                // Some code
            }
        }
        // Some default implementation
    }
}

Doesn't seem like much have been done, but the meaning of this code reveals the intent far better than the first version of the program.

More specifically, this code tells us that tax calculation is different depending on if it's applied to a company or a citizen. Then, within these two categories, different rules may be applied depending on the year.

All in all, it seems that the target of the tax (citizen or company) is a strong candidate for factoring out some objects !

class CompanyTax {
    getTotal() {
        if (this.year === 2024) {
            // Some code
        } else if (this.year === 2022) {
            // Some other code
        }
    }
}

class CitizenTax {
    getTotal() {
        if (this.year === 2023) {
        }
    }
}

class DefaultTax {
    getTotal() {
        // Default implementation
    }
}

That looks much more better, doesn't it ?

Now if you look closely, none of these classes implement any interface. Do we actually need one ?

It depends : is there any object that polymorphically needs to deal with the abstract idea of a Tax, regardless of the type of tax ?

Or is the type of tax always known by the object manipulating it ?

In the first case, we must extract a common interface to communicate that polymorphic usage. In the second case, we do not. Sometimes, indecisive objects are not extracted into polymorphic classes.

In this example, it is very likely that our object is used in a tax calculation system, and it is very likely that our program has no specific dependency on a specific type of tax as long as it can get the total. That is, if our program is written with proper object thinking.

Watch out for the conditionals !

Any conditional found in your code should be treated with suspicion, especially if that conditional is

  • Not inside a factory object or a static method that creates objects

  • Wrap the entire execution of a method

These are two strong hints that your objects have mental issues you need to deal with at once.

But what's wrong with Indecisive Objects ?

Didn't I say anything about it ? My bad... I thought it was plain obvious, especially after reading this entire book so far. But it doesn't hurt to state it clearly.

Indecisive Objects are hard to understand, because when you discover a code base and stumble upon it, you realize that you cannot understand how this object works unless you know the state it is in when it is executed.

Furthermore, some objects are mutable, meaning that the state of the object can potentially change during its lifecycle. That makes the code even harder to understand.

We strive to keep our code simple, linear and understandable. If we can lift decisions up the stack (in factories) and keep the core of the domain free as free of conditionals as possible, we can drastically reduce the cognitive charge of understanding the code.

Objects that are not indecisive are not only simpler to understand but also simpler to test. Since their cyclomatic complexity usually gets reduced to one, you have very few tests to write per object and the tests make more sense.

That is, if you write tests. You do write tests, right ? Right ?

It cannot be understated how impactful the plague of indecisive objects is on the understandability of a codebase. Anytime you see long methods with nested complicated conditionals, you can be certain that you have an object mixing both the selection of the context and the implementation of that context, the typical syndrome of indecisive objects. They're very difficult to refactor.

Last updated