Software Engineering

Design and Coding Principles

This guide briefly discusses and provides examples for the SOLID design principles. SOLID is a mnemonic for the five common design principles in this set. Although there are many other design principles, we will focus primarily on these five for the purposes of this course. Below are these five design principles together with examples of their use and misuse.

Single Responsibility Principle

The Single Responsibility Principle (SRP) states that each class in an object-oriented program should have exactly one clearly articulated responsibility or purpose. This helps to ensure that, when a change must be made to the design, it can be made in a single, easily-identified place. Relatedly, SRP also helps to prevent the creation of so-called “god objects”: objects containing massive numbers of methods or fields, thus defeating the purpose of object-oriented programming.

SRP-Breaking Example

A surprisingly common example of a violation of SRP comes from Oracle’s own Swing Tutorials. They’ll often include code outlines like the following:

public class Beeper extends JPanel implements ActionListener {
    ...
    //where initialization occurs:
        button.addActionListener(this);
    ...
    public void actionPerformed(ActionEvent e) {
        ...//Make a beep sound...
    }
}

This is a violation of the Single Responsibility Principle because the Beeper class has two responsibilities: it is part of the view (because it is a subclass of JPanel, which shows GUI widgets) and it is part of the controller (because it implements the ActionListener interface, which is used to receive a button click). The line

button.addActionListener(this);

is especially incriminating; listeners should not pass themselves to other objects!

Oracle’s tutorials are often written like this because, before Java 8, this style resulted in much shorter example code. The authors chose to use this poor design under the assumption that the readers would recognize it as an attempt at abbreviation and simply use the example to understand the Swing code. This technique works well for programmers versed in Java and object-oriented design, but less well for those still learning the topics.

SRP-Conforming Example

The above code could be corrected simply by writing the following:

public class Beeper extends JPanel {
    ...
    //where initialization occurs:
        button.addActionListener(
          (ActionEvent e) -> {
            ...//Make a beep sound...
          }
        );
    ...
}

Here, it’s not that the Beeper is an ActionListener; instead, the Beeper has an ActionListener. In the above code, the object passed to the button is a purpose-built listener object with the event-handling method and nothing else. The responsibility of handling the input is left to the listener, while the responsibility of constructing the GUI (including the creation of the listener) is left to the JPanel. The value of this may be easier to see if we think not about what the code does but how hard it will be to change in the future. If we wanted to separate the listener into its own class or generalize it in some way, it will be easier when the listener is not an embedded part of the Beeper class.

Open/Closed Principle

The Open/Closed Principle (OCP) states that code should be open for extension but closed for modification. That is, the behavior of the code should be extensible but it shouldn’t be necessary to change the code to make that happen. OCP helps to ensure that extensions to existing behavior can be made simply by adding new code rather than modifying a significant volume of existing code. Large projects which do not obey OCP require many changes throughout the software to accommodate an extension of behavior in a single place.

OCP-Breaking Example

Consider a game in which a player is awarded a certain number of points for collecting a variety of fruit. The OCP is broken in the following Player class implementation:

public class Player {
    ...
    public void scoreFruit(String fruitType) {
        if (fruitType.equals("apple")) {
            this.score += 5;
        } else if (fruitType.equals("cantaloupe")) {
            this.score += 20;
        } else if (fruitType.equals("pomegranite")) {
            this.score += 50;
        }
    }
}

In order to introduce a new type of fruit, the programmer would have to change the scoreFruit method. This means that new functionality must be implemented primarily by changing existing code rather than adding new code. This is an OCP infraction!

OCP-Conforming Example

Instead, we can write the Player class to accept an object which implements the interface Fruit. The Fruit object can dictate the particular number of points it is worth.

public class Player {
    ...
    public void scoreFruit(Fruit fruit) {
        this.score += fruit.getPointValue();
    }
}

We would then implement the interface with classes like Apple, Cantaloupe, and Pomegranite. Now, a new type of fruit can be introduced simply by writing a new class which implements the Fruit interface; the Player class need not change.

Liskov Substitution Principle

The Liskov Substitution Principle (LSP, so named after Dr. Barbara Liskov) states that, in a program, it should be possible to replace an object with another which has a more specific type without breaking the program. In other words, inheritance really does mean “is a”; if a Poodle is a Dog, then the Poodle can be used anywhere a Dog is required.

This statement is much more profound than it may seem initially. Carefully observing LSP leads to code which is semantically more meaningful, which allows the developers to use intuition to understand the software and modify its design. Code which does not observe LSP invariably forces the developer to confront and write code for nonsensical situations and track down counterintuitive bugs.

LSP-Breaking Example

Consider a program to track apartment rentals for a leasing office. Two important entities in the system are customers and leases. Each customer will have a lease with the office and will need to provide a name, phone number, and other such information. The lease includes an apartment number, a start date, and a duration. In order to get all of this information into the same place (so that a single object has all of these fields), one could write

public class Tenant {
    private String name;
    private String phoneNumber;
    ...
}
public class Lease extends Tenant {
    private String apartment;
    private Date startDate;
    private int durationInMonths;
    ...
}

Creating an object of type Lease will make an object that has storage for a name, phone number, apartment, start date, and lease duration. All of the memory we need has been allocated, but this design leaves much to be desired. To see this, consider what will happen in the future when these classes are used:

LSP-Conforming Example

The problem above arose because, although we got the memory allocation we desired, we accomplished this by saying things to our language runtime which simply aren’t true. Code expresses ideas and the idea expressed by the above code – that a lease is a kind of tenant – is nonsense. Instead, we should express ideas that match our problem domain:

public class Tenant {
    private String name;
    private String phoneNumber;
    ...
}
public class Lease {
    private String apartment;
    private Date startDate;
    private int durationInMonths;
    private Tenant tenant;
    ...
}

The above code says that a Lease has a Tenant (not that a Lease is a Tenant). This still has the first problem from above – that a lease cannot have two tenants – but the solution to that problem is much easier to identify. Further, the other problems described above no longer apply to this code: the same Tenant object may be shared among multiple Lease objects and Tenant methods may only be called on Tenant objects.

This example may seem somewhat elementary. The Liskov Substitution Principle is easier to obey with concrete ideas (such as tenants and leases) and somewhat harder to obey with classes representing abstract ideas (such as connection managers or business reports). One guiding principle, though, is that the “is a” relationship between objects must be defined in terms of the model with intuition as a guide. A Penguin is only a Bird, for instance, if the Bird interface has no fly() method; if such a method is desired, the class structure should be redesigned so that it makes sense. (One canonical example of this is the circle-ellipse problem.)

Interface Segregation Principle

The Interface Segregation Principle (ISP) states that interfaces should be small and purpose-driven and that no code should have to depend upon an interface (or part of an interface) that it doesn’t need. This principle is also related to the notion of a god object in that code which breaks ISP will often have singular, large interfaces capable of performing many different tasks. Observing ISP leads to code which is easier to understand and use from both sides of the interface: users of the interface can view the implementer as providing a single service while implementers of an interface need only worry about a small number of operations (rather than being required to implement many methods they don’t require).

ISP-Breaking Example

For a small infraction of ISP, we can turn to the design of Swing, a graphical framework for Java which predates JavaFX. In particular, an interface called MouseListener is used throughout the library to handle mouse clicks. This interface appears as follows (with documentation removed):

public interface MouseListener {
    public void mouseClicked(MouseEvent e);
    public void mouseEntered(MouseEvent e);
    public void mouseExited(MouseEvent e);
    public void mousePressed(MouseEvent e);
    public void mouseReleased(MouseEvent e);
}

As a consequence of this interface declaration, any class which wishes to listen for mouseClicked events must also provide implementations of the other four methods. To work around this problem, Swing includes a MouseAdapter class as follows:

public interface MouseAdapter implements MouseListener {
    public void mouseClicked(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
    public void mousePressed(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {}
}

Clients are then encouraged to extend MouseAdapter rather than implement MouseListener. This is indicative of the problem: it’s common to want to implement one of these methods while defaulting the others to no-ops. That is because the concerns of each mouse behavior are really separate.

It should be noted, though, that Swing is not the most egregious example of this problem. There is some interface separation in Swing, since events such as mouse wheel motion were implemented as separate interfaces (such as MouseWheelListener). The motivation for this appears to have more to do with history and backward compatibility than design, however; MouseWheelListener was added in Java 1.4 while MouseListener was part of Java 1.1.

ISP-Conforming Example

In JavaFX, the graphical components are designed to use a single interface, EventHandler, which is parameterized over the type of event for which it listens. That interface is declared as follows:

public interface EventHandler<T extends Event> { // T must be some kind of Event
    public void handle(T event);
}

The various graphical components then accept instances of that interface for a number of different purposes. For instance, every JavaFX graphical component has support for event handlers called onMouseEntered, onMouseExited, onMousePressed, etc. In this way, each event is handled separately rather as part of one large interface. This allows for new events to be added to JavaFX (e.g. onSwipeDown, added in JavaFX 2.2) without changing existing interfaces or requiring clients to implement methods they don’t use.

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) indicates that one should rely upon abstractions and not concretions. At a low level, this takes the form of declaring fields to be interface types (defining the interface if necessary) rather than class types. At a higher level, this requires that different portions of an application should rely upon well-defined interfaces to communicate without depending upon the specific behavior of that interface’s implementation.

Code which observes DIP is typically easier to separate into a collection of smaller modules. Code which does not observe DIP is often tightly interconnected and difficult to separate or modify. One common example of a high-level observation of DIP is the use of the MVC design pattern.

DIP-Breaking Example

Since MVC is an example of DIP in action, let us consider an application which breaks that pattern. This application will be a simple guess-the-number game. Consider the model code below:

public class GuessTheNumberModel {
    private int theNumber;
    private GUI gui;
    public GuessTheNumberModel(GUI gui) {
        this.theNumber = new Random().nextInt(10)+1;
        this.gui = gui;
    }
    public void guess(int number) {
        boolean correct = (this.theNumber == number);
        if (correct) {
            gui.notifyOfWin();
        } else {
            gui.notifyOfIncorrectGuess();
        }
    }
}

In this example, the model does not use listeners; instead, it actively calls the GUI when it has information to share. This is more simplistic, but makes the model harder to use. Consider these cases:

Note also that this violation of DIP has led to a violation of OCP: using this code in any context other than the GUI requires modifying the model class (rather than simply writing new code).

DIP-Conforming Example

The above example can be made to conform to DIP by introducing listeners to the model as we did in the MVC assignment. That assignment also introduced a model interface. This allows the GUI (or any other potential clients, like an automated tester) to communicate with the model via the abstraction of the interface rather than the concrete model class; it also allows the model to communicate with its users such as the GUI or tester via the listener type (which is also an abstraction) rather than knowing about those users directly. This is illustrated by the fact that you were able to implement the model separately from the GUI using only the interface available to you. In this way, the two parts of the software (GUI and model) communicate only through abstractions, allowing each to be understood separately from the other.