Abstraction and Indirection

27 May 2014

Object-oriented design is one of the most successful paradigms in computing. Almost all languages have objects these days and many have objects and/or classes as a central concept of their design. Proponents of object-oriented design will tell you that it allows you to make code that is decoupled, reusable, and maintainable. I believe that the success of object-oriented design is because it provides a heuristic for creating code that is abstract and indirect at the right places.

Looking past the heuristic

Why is it important to know the underlying qualities that object-orientation is supposed to lead you to? Having a firm grasp of the reasons that the object-oriented paradigm has gained so much success will enable you to use it more effectively. It will also enable you to use languages that have drastically different systems (like JavaScript) without having to recreate the exact syntax that is used in object-oriented languages.

Abstraction

Abstraction is a process of removing the exact details of a process or calculation and replacing them with an abstract concept instead. In object-oriented code, abstraction can be obtained by creating new classes that represent multiple related values or new methods that represent a single conceptual operation on the abstracted class. This allows developers to use these objects and methods by understanding the high-level concept and avoiding knowledge of the particular implementation details. This becomes essential in a code base that is too large for a single developer to keep in his or her head.

public class BadTaxCalculator {
    Set<Customer>          taxExempt;
    Map<Location, TaxCode> taxRates;

    public boolean isTaxExempt(Customer customer) {
        return taxExempt.contains(customer);
    }

    public BigDecimal getTaxRate(Item item, Location location) {
        return taxRates.get(location).getTaxFor(item);
    }

    public BigDecimal calculate(ArrayList<Item>       items,
                                ArrayList<BigDecimal> taxRates) {
        BigDecimal tax = BigDecimal.ZERO;
        for (int index = 0;
             index < items.size() && index < taxRate.size();
             index++) {
            Item item = items.get(index);
            tax = tax.add(item.getCost().times(taxRates.get(index)));
        }
        return tax;
    }
}

public class GoodTaxCalculator {
    BadTaxCalculator calculator;

    public BigDecimal calculate(Iterable<Item> items,
                                Location       locationOfPurchase,
                                Customer       customer) {
        if (calculator.isTaxExempt(customer)) return BigDecimal.ZERO;
        List<BigDecimal> taxRates = new ArrayList<>(items.size());
        for (int index = 0; index < items.size(); index++) {
            Item item = items.get(index);
            taxRates.add(calculator.getTaxRate(item, location);
        }
        return calculator.calculate(items, taxRates);
    }
}

In this example, the difference between the bad calculator and the good one is that using the bad api requires knowledge of the way that tax works. The better api only requires the user to know what they want to calculate the tax for. This abstraction allows programmers to work with the tax calculator without keeping all of the special cases in their heads, but it also concentrates tax-related code into a single object that can be maintained independently of its callers.

Indirection

Indirection is a mechanism for decoupling code to become more flexible. In object-oriented languages this can be done either explicitly using interfaces and/or base classes or implicitly with dynamic typing. By removing direct links between pieces of data and/or code, you create an ecosystem where individual components can be re-combined in interesting ways. This requires a programmer to separate code that relies on the fine details from code that is operating in a sufficiently abstract space to be reused.

public interface BadShareableResource {
    String getFacebookUrl();
}

public interface GoodShareableResource {
    String getUrl();
    String getTitle();
}

public class GoodResourceSharer implements BadShareableResource {
    GoodShareableResource resource;

    public String getFacebookUrl() {
        return "http://www.facebook.com/sharer/sharer.php?"
            + "u=" + resource.getUrl() + "&"
            + "t=" + resource.getTitle();
    }
}

The difference between the good and bad shareable resources in this example is a matter of indirection. In the first example, the details of Facebook's share URL format has to be included in every class that will implement the shareable interface, but in the second one, each resource can define it's own information and the sharer will combine the fields in the correct way. Breaking out indirection in a smart way will save a lot of code and potentially make maintenance much more easy since there is only one class that now needs to change if Facebook changes their URL structure or (more likely) a new social media site becomes popular and we want to add support for it across all of our products.