Software Design Principles

Abstraction

What

🏆 Can explain abstraction

Abstraction:

Most programs are written to solve complex problems involving large amounts of intricate details. It is impossible to deal with all these details at the same time. The guiding principle of abstraction stipulates that we capture only details that are relevant to the current perspective or the task at hand. For example, within a certain software component, we might deal with a ‘user’ data type, while ignoring the details contained in the user data item. These details have been ‘abstracted away’ as they do not affect the task of that software component. This is called data abstraction. On the other hand, control abstraction abstracts away details of the actual control flow to focus on tasks at a simplified level. For example, print(“Hello”) is an abstraction of the actual output mechanism within the computer.

Abstraction can be applied repeatedly to obtain higher and higher levels of abstractions. For example, a File is a data item that is at a higher level than an array and an array is at a higher level than a bit. Similarly, execute(Game) is at a higher level than print(Char) which is at a higher than an Assembly language instruction MOV.

Coupling

What

🏆 Can explain coupling

Coupling is a measure of the degree of dependence between components, classes, methods, etc. Low coupling indicates that a component is less dependent on other components. In the example below, design A appears to have a more coupling between the components than design B.

Example:

Discuss the coupling levels of alternative designs x and y.

Overall coupling levels in x and y seem to be similar (neither has more dependencies than the other). (Note that the number of dependency links is not a definitive measure of the level of coupling. Some links may be stronger than the others.). However, in x, A is highly-coupled to the rest of the system while B, C, D, and E are standalone (do not depend on anything else). In y, no component is as highly-coupled as A of x. However, only D and E are standalone.

Why

🏆 Can justify the need to reduce coupling

Highly coupled (also referred to as tightly coupled or strongly coupled) systems display the following disadvantages:

  • A change in one module usually forces changes in other modules coupled to it (i.e. a ripple effect).
  • Integration is harder because multiple components coupled with each other have to be integrated at the same time.
  • Testing and reuse of the module is harder due to its dependence on other modules.

In the case of high-coupling (i.e. relatively high dependency), a change in one component may require changes in other coupled components. Therefore, we should strive to achieve a low-coupled design.

Explain the link (if any) between regression and coupling.

When the system is highly-coupled, the risk of regression is higher too. When component A is modified, all components ‘coupled’ to component A risk ‘unintended behavioral changes’.

Discuss the relationship between coupling and testability (testability is a measure of how easily a given component can be tested).

Coupling decreases testability as it is difficult to isolate highly-coupled objects.

Choose the least correct statement.

  • a. As coupling increases, testability decreases.
  • b. As coupling increases, the risk of regression increases.
  • c. As coupling increases, the value of automated regression testing increases.
  • d. As coupling increases, integration becomes easier as everything is connected together.
  • e. As coupling increases, maintainability decreases.
  • a. As coupling increases, testability decreases.
  • b. As coupling increases, the risk of regression increases.
  • c. As coupling increases, the value of automated regression testing increases.
  • d. As coupling increases, integration becomes easier as everything is connected together.
  • e. As coupling increases, maintainability decreases.

Explanation: High coupling means either more components require to be integrated at once in a big-bang fashion (increasing the risk of things going wrong) or more drivers and stubs are required when integrating incrementally.

How

🏆 Can reduce coupling

In general component A is coupled to B if a change to B could potentially require a change in A.

Some examples:

  • component A has access to the internal structure of component B (this results in a very high level of coupling);
  • component A and B depend on the same global variable;
  • component A calls component B;
  • component A receives an object of component B as a parameter or a return value;
  • component A inherits from component B;
  • components A and B are required to follow the same data format or communication protocol.

Which of these does not indicate a coupling between components A and B?

  • a. component A has access to internal structure of component B.
  • b. component A and B are written by the same developer.
  • c. component A calls component B.
  • d. component A receives an object of component B as a parameter.
  • e. component A inherits from component B.
  • f. components A and B have to follow the same data format or communication protocol.
  • a. component A has access to internal structure of component B.
  • b. component A and B are written by the same developer.
  • c. component A calls component B.
  • d. component A receives an object of component B as a parameter.
  • e. component A inherits from component B.
  • f. components A and B have to follow the same data format or communication protocol.

Explanation: Being written by the same developer does not imply a coupling.

Types of Coupling

🏆 Can identify types of coupling

Temporal coupling etc.

Cohesion

What

🏆 Can explain cohesion

Cohesion is a measure of how strongly-related and focused the various responsibilities of a component are. A highly-cohesive component keeps related functionalities together while keeping out all other unrelated things.

Why

🏆 Can justify the need to increase cohesion

The following are some disadvantages of low cohesion (or “weak cohesion”).

  • Impedes the understandability of modules as it is difficult to express module functionalities at a higher level.
  • Difficulty in maintaining modules because a localized adjustment in the requirements can result in changes spread across the system since requirement-related functionality is implemented across many components of the system.
  • Modules become less reusable because they do not represent logical units of functionality.

One should strive for high cohesion to facilitate code maintenance and reuse.

How

🏆 Can increase cohesion

Cohesion can be present in many forms. For example,

  • Code related to a single concept is kept together, e.g. the Student component handles everything related to students.
  • Code that is invoked close together in time is kept together, e.g. all code related to initializing the system is kept together.
  • Code that manipulates the same data structure is kept together, e.g. the GameArchive component handles everything related to the storage and retrieval of game sessions.

The components in the following sequence diagram show low cohesion because user interactions are handled by many components. Its cohesion can be improved by moving all user interactions to the UI component.

Example:

Compare the cohesion of the following two versions of the EmailMessage class. Which one is more cohesive and why?

// version-1
class EmailMessage {
    private String sendTo;
 	  private String subject;
    private String message;

    public EmailMessage(String sendTo, String subject, String message) {
        this.sendTo = sendTo;
        this.subject = subject;
        this.message = message;
    }

    public void sendMessage() {
        // sends message using sendTo, subject and message
    }
}

// version-2
class EmailMessage {
    private String sendTo;
    private String subject;
    private String message;
    private String username;

    public EmailMessage(String sendTo, String subject, String message) {
        this.sendTo = sendTo;
        this.subject = subject;
        this.message = message;
    }

    public void sendMessage() {
        // sends message using sendTo, subject and message
    }

    public void login(String username, String password) {
        this.username = username;
        // code to login
    }
}

Version 2 is less cohesive.

Explanation: Version 2 is handling functionality related to login, which is not directly related to the concept of ‘email message’ that the class is supposed to represent. On a related note, we can improve the cohesion of both versions by removing the sendMessage functionality. Although sending message is related to emails, this class is supposed to represent an email message, not an email server.

Open-Closed Principle

What

🏆 Can explain open-closed principle (OCP)

While it is possible to isolate the functionalities of a software system into modules, there is no way to remove interaction between modules. When modules interact with each other, coupling naturally increases. Consequently, it is harder to localize any changes to the software system. In 1988, Bertrand Meyer proposed a guiding principle to alleviate this problem. The principle, known as the open-closed principle, states: “A module should be open for extension but closed for modification”. That is, modules should be written so that they can be extended, without requiring them to be modified. In other words, changing what the modules do without changing the source code of the modules.

In object-oriented programming, these two seemingly opposing requirements can be achieved in various ways. This often requires separating the specification (interface) of a module from its implementation.

Example:

The behavior of the CommandQueue class can be altered by adding more concrete Command subclasses. For example, by including a Delete class alongside List, Sort, and Reset, the CommandQueue can now perform delete commands without modifying its code at all. Indeed, its behavior was extended without having to open up and modify its code. Hence, it was open to extensions, but closed to modification.

The behavior of a Java generic class can be altered by passing it a different class as a parameter. In the code below, the ArrayList class behaves as a container of Students in one instance and as a container of Admin objects in the other instance, without having to change its code. That is, the behavior of the ArrayList class is extended without modifying its code.

ArrayList students = new ArrayList< Student >();
ArrayList admins = new ArrayList< Admin >();  	

Which of these is closest to the meaning of the open-closed principle?

  • a. We should be able to change a software module’s behavior without modifying its code.
  • b. A software module should remain open to modification as long as possible.
  • c. A software module should be either open to modification and closed to extension.
  • d. Open source software rocks. Closed source software sucks.
  • a. We should be able to change a software module’s behavior without modifying its code.
  • b. A software module should remain open to modification as long as possible.
  • c. A software module should be either open to modification and closed to extension.
  • d. Open source software rocks. Closed source software sucks.

Explanation: Please refer the handout for the definition of OCP.

Dependency Inversion Principle

What

🏆 Can explain dependency inversion principle (DIP)

The Dependency Inversion Principle states that,

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

Example:

In design (a), the higher level class Payroll depends on the lower level class Employee, a violation of DIP. In design (b), both Payroll and Employee depends on the Payee interface (note that inheritance is a dependency).

Design (b) is more flexible (and less coupled) because now the Payroll class need not change when the Employee class changes.

Which of these statements is true about the Dependency Inversion Principle.

  • a. It can complicate the design/implementation by introducing extra abstractions, but it has some benefits.
  • b. It is often used during testing, to replace dependencies with mocks.
  • c. It reduces dependencies in a design.
  • d. It advocates making higher level classes to depend on lower level classes.
  • a. It can complicate the design/implementation by introducing extra abstractions, but it has some benefits.
  • b. It is often used during testing, to replace dependencies with mocks.
  • c. It reduces dependencies in a design.
  • d. It advocates making higher level classes to depend on lower level classes.

Explanation: Replacing dependencies with mocks is Dependency Injection, not DIP. DIP does not reduce dependencies, rather, it changes the direction of dependencies. Yes, it can introduce extra abstractions but often the benefit can outweigh the extra complications.