Design Principles

Abstraction

What

Abstraction: Abstraction is a technique for dealing with complexity. It works by establishing a level of complexity on which a person interacts with the system, suppressing the more complex details below the current level.

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.

Ignoring lower level data items and thinking in terms of bigger entities is called data abstraction.

📦 Within a certain software component, we might deal with a user data type, while ignoring the details contained in the user data item such as name, and date of birth. These details have been ‘abstracted away’ as they do not affect the task of that software component.

Control abstraction abstracts away details of the actual control flow to focus on tasks at a simplified level.

📦 print(“Hello”) is an abstraction of the actual output mechanism within the computer.

For example,

Abstraction can be applied repeatedly to obtain progressively higher levels of abstractions.

📦 An example of different levels of data abstraction: 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.

📦 An example of different levels of control abstraction: execute(Game) is at a higher level than print(Char) which is at a higher than an Assembly language instruction MOV.

Coupling

What

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. High coupling (aka tight coupling or strong coupling) is discouraged due to the following disadvantages:

  • Maintenance is harder because a change in one module could cause 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 example below, design A appears to have a more coupling between the components than design B.

How

X is coupled to Y if a change to Y can potentially require a change in X.

📦 If Foo class calls the method Bar#read(), Foo is coupled to Bar because a change to Bar can potentially (but not always) require a change in the Foo class  e.g. if the signature of the Bar#read() is changed, Foo needs to change as well, but a change to the Bar#write() method may not require a change in the Foo class because Foo does not call Bar#write().

class Foo{
    ...
    new Bar().read();
    ...
}

class Bar{
    void read(){
        ...
    }
    
    void write(){
        ...
    }
}

📦 Some examples of coupling: A is coupled to B if,

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

Types of Coupling

Some examples of different coupling types:

  • Content coupling: one module modifies or relies on the internal workings of another module  e.g., accessing local data of another module
  • Common/Global coupling: two modules share the same global data
  • Control coupling: one module controlling the flow of another, by passing it information on what to do  e.g., passing a flag
  • Data coupling: one module sharing data with another module  e.g. via passing parameters
  • External coupling: two modules share an externally imposed convention  e.g., data formats, communication protocols, device interfaces.
  • Subclass coupling: a class inherits from another class. Note that a child class is coupled to the parent class but not the other way around.
  • Temporal coupling: two actions are bundled together just because they happen to occur at the same time  e.g. extracting a contiguous block of code as a method although the code block contains statements unrelated to each other

Cohesion

What

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.

Higher cohesion is better. Disadvantages of low cohesion (aka weak cohesion):

  • Impedes the understandability of modules as it is difficult to express module functionalities at a higher level.
  • Lowers maintainability because a module can be modified due to unrelated causes  (reason: the module contains code unrelated to each other) or many many modules may need to be modified to achieve a small change in behavior  (reason: because the code realated to that change is not localized to a single module).
  • Lowers reusability of modules because they do not represent logical units of functionality.

How

Cohesion can be present in many forms. Some examples:

  • 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.

Open-Closed Principle

What

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. The Open-Close Principle aims to alleviate this problem.

Open-Closed Principle (OCP): 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. -- proposed by Bertrand Meyer

In object-oriented programming, OCP can be achieved in various ways. This often requires separating the specification (i.e. interface) of a module from its implementation.

📦 In the design given below, 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. That is, its behavior was extended without having to 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 >();  	

Dependency Inversion Principle

What

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.