Orthogonality In Software Design

Orthogonality In Software Design

Achieving orthogonality in the design of software is an investment into the future of the resulting software system because orthogonality increases its long-term fitness in terms of quality attributes such as flexibility, testability, and extensibility.

In this blog post, we’ll have a look at the concept of orthogonality in context of software engineering and how it can help us improve the quality attributes of the software we write. We’ll then explore two of the concepts that contribute to building orthogonal software systems.

Orthogonality And Its Benefits

Orthogonality is a term borrowed from geometry, where two lines are orthogonal if they meet at right angles. Within the realms of software engineering, the term is used to refer to a kind of independence between the components in a software system. For example, say you have three modules and need to update one of them – in this case, the module is orthogonal to the others if a change made to it does not ripple through the other two. On the other hand, if you make a change to one module and you end up having to make changes in the other two modules as well, then the modules are not orthogonal.

Building orthogonal software modules can be tricky, but the effort is well worth it because orthogonality entails some important benefits for the quality attributes of the resulting software system:

  • To build orthogonal modules, each module has to have a single, well-defined responsibility and needs to encapsulate the functionality required to fulfil this responsibility behind a neat API. This promotes reuse – modules can be easily combined in new and innovative ways if there is no responsibility (and, hence, functionality) overlap, and developers feel better about reusing existing modules if they know they don’t have to deal with a module’s internal complexity because it’s hidden behind an API.
  • Reusing modules, in turn, facilitates adhering to the DRY principle (Don’t Repeat Yourself).
  • In orthogonal modules, changes are localized. To achieve a certain goal, only one module has to be modified, and those modifications will not break things or otherwise necessitate change in other modules. Consequently, the system’s overall flexibility to adapt to new or changed requirements increases, and so does its extensibility, as developers can extend functionality with higher confidence.
  • As an extension of the above, bugs are also isolated and can be fixed with less effort – and with more confidence that one fix will not magically introduce other bugs in completely unrelated parts of the system (a scenario that any developer will dread).
  • It’s easier to write tests for and run tests on orthogonal modules, so the testability of the system as a whole increases.

Low coupling and high cohesion are both crucial to achieving orthogonality in a software system, so we’re going to cover the basics of both in the upcoming sections. Armed with that knowledge, we can then turn our attention to specific guidelines and principles on how to achieve both low coupling and high cohesion. This will be the topic of the next blog post.

Low Coupling

The term low coupling can be found in pretty much any discussion on software design (often in combination with the term high cohesion). Coupling between classes or modules is a measure for how interconnected those classes or modules are. High coupling is considered bad because highly coupled classes or modules have to have a lot of knowledge about the internals and/or return values of the other, making it very difficult to change one without also having to change the other. To illustrate this, let’s have a look at the following piece of Java code (we’ll see an improved version of this a bit further down the line):

public class Customer1 {

    private final long id;
    private final String name;
    private final List<Invoice> invoices;

    public Customer1(long id, String name) {
        this.id = id;
        this.name = name;
        this.invoices = new ArrayList<>();
        initInvoices();
    }

    public void initInvoices() {

        var invoiceReader = new InvoiceReader1("invoices/%d.csv".formatted(id));
        try {
            var bufferedReader = invoiceReader.getReader();
            bufferedReader.lines().forEach(line -> {
                var invoiceElements = line.split(",");
                var date = invoiceElements[0];
                var subject = invoiceElements[1];
                var amount = Double.parseDouble(invoiceElements[2]);
                invoices.add(new Invoice(date, subject, amount));
            });
        } catch (FileNotFoundException e) {
            // Exception handling
        }

    }

    // Truncated

}

(This would become even uglier if you started to mix in a bit of error handling, like what to do if an invoice’s amount was not stored in a string that can be parsed into a double – but the code is sufficiently unelegant to drive home the point.)

Here, the Customer class uses an InvoiceReader class that returns CSV-formatted strings containing invoice data (a date, a subject, and an amount), which the Customer class uses to initialize some Invoice objects. To do so, Customer needs to know that the strings delivered by InvoiceReader are CSV-formatted, and which piece of information sits where in each line.

The way the invoice initialization works here is a nice example for very poor separation of concerns: The Customer class handles a task it shouldn’t be responsible for, and in doing so, becomes very tightly coupled to the InvoiceReader class as well as the values it returns. This is quite nasty because of the following reasons:

  • The code is somewhat harder to read and understand because you wouldn’t expect the Customer class to encapsulate knowledge related to the storage format of invoices
  • Changes made to InvoiceReader or the invoice format will ripple through the Customer class, necessitating the latter be modified, too
  • Because the knowledge about the underlying invoice storage format and how to create Invoice objects is encapsulated in the Customer class, it cannot be reused separately
  • An extension of the previous point, and maybe even more of an issue: The knowledge and its corresponding functionality cannot be tested separately

Therefore, building classes or modules to be loosely coupled benefits the following quality attributes:

  • Readability: Code becomes easier to read and understand
  • Flexibility, maintainability: Modifications made to fix bugs or implement a new feature are localized
  • Testability: It’s a lot easier (and a lot more fun) to set up nice tests for decoupled classes or modules
  • Reusability: Classes and modules can be easily combined in new and innovative ways

An additional benefit arises from the fact that designing classes and modules to be decoupled forces you to think about responsibility (essentially: “Which piece of functionality should reasonably go where?”) and proper separation of concerns, which further improves the system’s quality attributes. And, if you get the responsibilities right, you’ll also end up with a system in which classes or modules all have high cohesion That’s why the terms low coupling and high cohesion are so often used together – one influences the other. (Interestingly, low coupling and high cohesion don’t necessarily seem to be logical consequences of one another – you could build highly cohesive modules and still couple them together very tightly, and vice-versa, you could build loosely coupled modules that still have terribly cohesion. Therefore, both should be treated as individual goals to achieve.)

High Cohesion

Cohesion is a measure of how closely pieces of code that contribute to the same goal relate to each other. For example, if a class or module contains code that accomplishes a lot of different things, then this class or module is said to have low cohesion. On the other hand, if all the code in a class or module is dedicated to the same functional goal, then that class or module has high cohesion. For example, the Customer class shown in the example above has low cohesion because it does something it should not be responsible for (parsing invoices from an underlying storage format, and encapsulating the knowledge for how to do so). Let’s have a look at an improved version of the previous example:

public class Customer2 {

    private final long id;
    private final String name;
    private final List<Invoice> invoices;

    public Customer2(long id, String name) {
        this.id = id;
        this.name = name;
        this.invoices = InvoiceReader2.getInstance().getInvoicesByCustomerID(id);
    }

    // Truncated

}

The InvoiceReader class now directly returns a list of Invoice objects. Here’s how it looks:

public class InvoiceReader2 {

    private static InvoiceReader2 invoiceReader2;

    private InvoiceReader2() {
    }

    public static InvoiceReader2 getInstance() {
        if (Objects.isNull(invoiceReader2)) {
            invoiceReader2 = new InvoiceReader2();
        }
        return invoiceReader2;
    }

    public List<Invoice> getInvoicesByCustomerID(long customerID) {

        try {
            return Files.lines(Paths.get(String.format("invoices/%d.csv", customerID)))
                    .map(invoiceString -> {
                        var invoiceData = invoiceString.split(",");
                        return new Invoice(invoiceData[0], invoiceData[1], Double.parseDouble(invoiceData[2]));
                    })
                    .collect(Collectors.toList());
        } catch (IOException e) {
            // Do some logging, or maybe rethrow exception to notify caller...
        }

        return Collections.emptyList();

    }

}

This code is much easier to read and understand because it conforms to the expectation you’d probably have about where to find knowledge related to reading invoice data – in a class called InvoiceReader. The InvoiceReader class has high cohesion because its entire functionality is dedicated to only one thing, and so the effects of changes to that thing are limited to only that class – if we decided to, say, read invoice data from a database or modify the invoice format, the ensuing changes would be restricted to the InvoiceReader class, and the Customer class consuming it would be entirely indifferent to them. Hence, Customer and InvoiceReader are now loosely coupled. This increases the flexibility and maintainability of InvoiceReader, and also increases its functionality’s reusability. On top of that, testability is also improved – because the invoice reading functionality is now nicely encapsulated, it can be tested separately using module-level (or unit-level) tests, and such tests tend to be a lot easier to specify and implement than integration-level tests.

Wrap-Up And Upcoming Topics

Building loosely coupled software modules with high cohesion contributes to achieving orthogonality and increases the quality attributes of the resulting software system. That’s great for developers since working on such a system is simply a lot more fun; it’s great for management since their developers will be able to spend more of their time building the things that deliver value to clients rather than fixing bugs; and it’s great for clients because they’ll get more valuable features and a more reliable system to interact with.

To achieve low coupling and high cohesion, it’s sometimes helpful to have a set of guidelines or principles that make flaws in existing classes or modules more obvious (or that help prevent them from occurring in the first place). It’s the job of the next blog post to provide you with just such guidelines – they will help you ask the right questions about some class or module you’ve just written or you were given to work with, and implementing the answers to those questions will help you reduce coupling and increase cohesion for the involved classes or modules.