Principles

This is a printer-friendly version. It omits exercises, optional topics (i.e., four-star topics), and other extra content such as learning outcomes.

Single Responsibility Principle

Single Responsibility Principle (SRP): A class should have one, and only one, reason to change. -- Robert C. Martin

If a class has only one responsibility, it needs to change only when there is a change to that responsibility.

Consider a TextUi class that does parsing of the user commands as well as interacting with the user. That class needs to change when the formatting of the UI changes as well as when the syntax of the user command changes. Hence, such a class does not follow the SRP.

Gather together the things that change for the same reasons. Separate those things that change for different reasons. Agile Software Development, Principles, Patterns, and Practices by Robert C. Martin

Open-Closed Principle

The Open-Close Principle aims to make a code entity easy to adapt and reuse without needing to modify the code entity itself.

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 >();

Liskov Substitution Principle

Liskov Substitution Principle (LSP): Derived classes must be substitutable for their base classes. -- proposed by Barbara Liskov

LSP sounds same as substitutability but it goes beyond substitutability; LSP implies that a subclass should not be more restrictive than the behavior specified by the superclass. As we know, Java has language support for substitutability. However, if LSP is not followed, substituting a subclass object for a superclass object can break the functionality of the code.

Paradigms → OOP → Inheritance →

Substitutability

Every instance of a subclass is an instance of the superclass, but not vice-versa. As a result, inheritance allows substitutability : the ability to substitute a child class object where a parent class object is expected.

an Academic is an instance of a Staff, but a Staff is not necessarily an instance of an Academic. i.e. wherever an object of the superclass is expected, it can be substituted by an object of any of its subclasses.

The following code is valid because an AcademicStaff object is substitutable as a Staff object.

Staff staff = new AcademicStaff (); // OK

But the following code is not valid because staff is declared as a Staff type and therefore its value may or may not be of type AcademicStaff, which is the type expected by variable academicStaff.

Staff staff;
...
AcademicStaff academicStaff = staff; // Not OK

Suppose the Payroll class depends on the adjustMySalary(int percent) method of the Staff class. Furthermore, the Staff class states that the adjustMySalary method will work for all positive percent values. Both Admin and Academic classes override the adjustMySalary method.

Now consider the following:

  • Admin#adjustMySalary method works for both negative and positive percent values.
  • Academic#adjustMySalary method works for percent values 1..100 only.

In the above scenario,

  • Admin class follows LSP because it fulfills Payroll’s expectation of Staff objects (i.e. it works for all positive values). Substituting Admin objects for Staff objects will not break the Payroll class functionality.
  • Academic class violates LSP because it will not work for percent values over 100 as expected by the Payroll class. Substituting Academic objects for Staff objects can potentially break the Payroll class functionality.

The Rectangle#resize() can take any integers for height and width. This contract is violated by the subclass Square#resize() because it does not accept a height that is different from the width.

class Rectangle {
    ...
    /** sets the size to given height and width*/
    void resize(int height, int width){
        ...
    }
}


class Square extends Rectangle {
    
    @Override
    void resize(int height, int width){
        if (height != width) {
            //error
       }
    }
}

Now consider the following method that is written to work with the Rectangle class.

void makeSameSize(Rectangle original, Rectangle toResize){
    toResize.resize(original.getHeight(), original.getWidth());
}

This code will fail if it is called as maekSameSize(new Rectangle(12,8), new Square(4, 4)) That is, Square class is not substitutable for the Rectangle class.

Separation of Concerns Principle

Separation of Concerns Principle (SoC): To achieve better modularity, separate the code into distinct sections, such that each section addresses a separate concern. -- Proposed by Edsger W. Dijkstra

A concern in this context is a set of information that affects the code of a computer program.

Examples for concerns:

  • A specific feature, such as the code related to add employee feature
  • A specific aspect, such as the code related to persistence or security
  • A specific entity, such as the code related to the Employee entity

Applying SoC reduces functional overlaps among code sections and also limits the ripple effect when changes are introduced to a specific part of the system.

If the code related to persistence is separated from the code related to security, a change to how the data are persisted will not need changes to how the security is implemented.

This principle can be applied at the class level, as well as on higher levels.

The n-tier architecture utilizes this principle. Each layer in the architecture has a well-defined functionality that has no functional overlap with each other.

Design → Architecture → Styles → n-Tier Style →

What

In the n-tier style, higher layers make use of services provided by lower layers. Lower layers are independent of higher layers. Other names: multi-layered, layered.

Operating systems and network communication software often use n-tier style.

This principle should lead to higher cohesion and lower coupling.

Design → Design Fundamentals → 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.

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.

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

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

Discuss the relationship between coupling and testability.

Coupling decreases testability because if the SUT is coupled to many other components it becomes difficult to test the SUI in isolation of its dependencies.

Choose the correct statements.

  • 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)(b)(c)(d)(e)

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.

Design → Design Fundamentals → 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):

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

Law of Demeter

Law of Demeter (LoD):

  • An object should have limited knowledge of another object.
  • An object should only interact with objects that are closely related to it.

Also known as

  • Don’t talk to strangers.
  • Principle of least knowledge

More concretely, a method m of an object O should invoke only the methods of the following kinds of objects:

  • The object O itself
  • Objects passed as parameters of m
  • Objects created/instantiated in m (directly or indirectly)
  • Objects from the direct association of O

The following code fragment violates LoD due to the reason: while b is a ‘friend’ of foo (because it receives it as a parameter), g is a ‘friend of a friend’ (which should be considered a ‘stranger’), and g.doSomething() is analogous to ‘talking to a stranger’.

void foo(Bar b) {
    Goo g = b.getGoo();
    g.doSomething();
}

LoD aims to prevent objects navigating internal structures of other objects.

An analogy for LoD can be drawn from Facebook. If Facebook followed LoD, you would not be allowed to see posts of friends of friends, unless they are your friends as well. If Jake is your friend and Adam is Jake’s friend, you should not be allowed to see Adam’s posts unless Adam is a friend of yours as well.