SOLID Principles: Dependency Inversion

The Dependency Inversion Principle “D”

High Level Modules should not depend on low level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Overview

The word inversion is a misfit in the 2020s but it has some history around it. Most software were developed using procedural methodologies in the 80s and 90s; in this approach high level business functionalities are broken into low level reusable functions. Although this approach creates good reusable low level functions but it makes high level business functions to require rework when any change is done in low level functions. The term dependency ‘inversion’ was used in the late 90s to say that low level modules should depend on high level modules (main business function) via abstraction in Object Oriented Analysis & Design.

Thus, inverting the way dependencies should be defined is called Dependency Inversion.

Layered Design

A good software architecture defines different layers with specific responsibilities. The bottom layers are more generic & reusable and functionalities tend to become more specific to business use cases as we move up the layers. E.g.

Figure-1 Layered Design
  • The lowest layer contains Infrastructure or Utilities like file handling, DB repo, security.
  • The middle layer contains reusable business services or models. The functionalities defined here are business functions which are independent of business use cases.
  • The top layer contains business use cases which are orchestrated using the middle layer. These are the functionalities understood by users of the application.

The different layers are defined in different classes, packages/namespaces but they are still tightly coupled. To reduce the coupling and enable higher reuse of Business Services, Different layers can invoke the concrete classes via abstraction, like in the Figure-2:

Figure-2 Layered Design

Conclusion

Defining module dependencies via Abstraction helps in maintaining low coupling while keeping the code reusable.

Proper implementation of Dependency Inversion principle requires a broad understanding of the Domain for which application is being developed. The key decision is to segregate functionality between middle layer (reusable business services) and top layer (business user cases).

SOLID Principles: Interface Segregation

Interface segregation is another technique that helps in keeping the system loosely coupled by dividing the functionality into small components.

The Interface Segregation Principle “I”

It states that no client should be forced to depend on methods it does not use. In other words, only relevant methods and functionalities should be exposed to clients and clients should have ability to pick and choose what’s relevant to them.

The problem with creating large Interfaces and hence heavy classes is that every client may not require all the functionality offered by the large Interface but they end up including things they don’t use. This leads to a very common issue in the Software Life-cycle where things break as a side-effect of some other change. Unexpected errors may occur for a client when there are changes in certain areas of the underlying application (which are not even being used by the client).

Interface Segregation can be considered an extension of Single Responsibility Principle which helps in segregating related but different functionalities.

Consider the example of a generalized E-Commerce platform which can be utilized for sale & delivery of physical goods and electronic subscription.

Classes which handle Delivery can be defined like this:

Figure 1

Some of OrderDelivery methods like assignPostalServiceProvider() may not be applicable for ElectronicDelivery. Clients that only require ElectronicDelivery will unnecessarily get the APIs specific to Physical Delivery.

class ElectronicDelivery implements OrderDelivery {

  @Override
  public void assignPostalServiceProvider(String provider) {
    //Do nothing
  }
}

Any changes related to Physical Delivery in the OrderDelivery interface, would require updates in the ElectronicDelivery concrete implementation too. It is useless but necessary change that can be avoided with proper segregation of Interfaces like:

Figure 2

The way classes are defined in Figure 2 is much cleaner and each class contains only relevant behavior.

Conclusion

When each class contains only relevant functionality, then client applications are never impacted by unused dependencies. Interface Segregation helps in reducing side-effect bugs during the maintenance of Application.

SOLID Principles: Behavioral Subtyping

Also known as Liskov substitution principle, it states that a derived entity should be behavioral extension of the base entity and not just syntactical extension.

The Liskov Substitution Principle “L”

This is an often overlooked principle while defining class hierarchies. Base & derived classes should have behavioral relation between them and Inheritance should not be applied just for the sake of reusing code.

As per LSP, objects of Base class should be replaceable with objects of Derived class without any change in the behavior or correctness of the application.

Inheritance signifies an Is-A relation between the derived class and the base class but at times, it can be misleading. Couple of classic examples:

  1. Square extends Rectangle: In basic geometric terms a square is a specific form of rectangle, so it might seem right for Square class to derive from Rectangle.
    • In object oriented design terms, a Rectangle has 2 sides but a Square has only 1.
    • When Square class extends Rectangle, it gets more variables than it needs and thus would need to do some work-around to achieve true behavior of a single side square.
    • Consider the following code snippet defying Behavioral Subtyping. setHeight() of Rectangle changes the value of variable height but setHeight() of Square changes both height & width. If clients replaces a Rectangle object with a Square object, then they will observe (unexpectedly) that setHeight() changes both height & width.
public class Rectangle {

  protected int height;
  protected int width;

  public int getHeight() {
    return height;
  }

  public void setHeight(int height) {
    this.height = height;
  }

  public int getWidth() {
    return width;
  }

  public void setWidth(int width) {
    this.width = width;
  }
}

public class Square extends Rectangle {

  @Override
  public int getHeight() {
    return super.getHeight();
  }

  @Override
  public void setHeight(int height) {
    super.setHeight(height);
    super.setWidth(height);
  }

  @Override
  public int getWidth() {
    return super.getWidth();
  }

  @Override
  public void setWidth(int width) {
    super.setHeight(width);
    super.setWidth(width);
  }

}
  1. Stack extends Queue: Both of these data structures have put() and get() methods. The put() method for them is same because it adds an element at the end of the data structure. The get() method is different; it implements FIFO in case of Queue and LIFO in case of Stack.
    • Defining Stack to extend Queue does provides code re usability but breaks the Liskov substitution principle because a Queue cannot be replaced with Stack without changing the underlying behavior.
    • A better class design would be something like the following. An abstract class AbstractCollection contains the common code of put() method and both Stack and Queue extend the abstract class to add concrete definition of get() method.
Figure 1

As per Wikipedia, Liskov substitution principle says “if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program“.

In practice, applying this principle helps in deciding between Inheritance (Is-a) and Composition (Has-a) relation between classes.

Consider the following example of Rolling File Logger.

The objective is to add log messages to a file and roll the file based on the size of file. The library needs to support another functionality of rolling the log file based on time interval.

In this first approach (Figure 2) which defies Behavioral Subtyping, these two functionalities can be achieved by TimeRollingFileLogger extending SizeRollingFileLogger and overriding the method isRolloverRequired() which determines when to roll the log file.

Figure 2

A better design is to separate the Rolling Strategy from the FileLogger like:

Figure 3

In the second approach of Figure-3, FileLogger has-a RollingStrategy and Interface RollingStrategy is implemented by three different concrete classes for three specific strategies viz None, SizeBased and TimeBased.

The second approach better adheres to the behavioral relation between FileLogger and it’s rolling strategies in term of Object Oriented Design.

Conclusion

Behavioral Subtyping OR Liskov Substitution Principle helps in defining proper Object Oriented Design relation between classes by distinguishing Composition and Inheritance.

Interfaces in Java

Interface in Java is a concept to separate contract (what) from implementation (how).

The keyword interface creates an entity which defines how a class is going to look like with method names, their list & type of arguments and their return type. Interface does not define any method body.

One or more classes can implement the interface to provide the actual method body for all the methods declared in the interface . Since interfaces do not contain any method body, Java does not allow creation of objects of interface. Objects can be created for class that implements the interface.

interface

Similar to inheritance, when defining class for interface we specify name of the interface it is implementing. Here we use the keyword implements to define the class.

A class can implement multiple interfaces by separating Interface names using comma after the implements keyword. An object of such class can be upcasted to any of the interfaces. This is Java’s way of multiple inheritance which is directly not allowed with extends keyword.

multiple-inheritance

Abstract Class

We have discussed about classes and interfaces, there is another entity in Java called Abstract Class which falls into same category but is midway between class and interface. Mid-way because we can provide definition of some methods (like classes) and leave some as abstract or as declaration  (like interfaces). Java does not allow instantiation of abstract class but allows it to be extended like any other class using extends keyword.

abstract-class

In this example, we have created a class TennisPlayer by inheriting from abstract class Person and implementing interface Player. TennisPlayer gets eat() capability from Person but is forced to define work() capability as mandated in same Person abstract class. Similarly, TennisPlayer is forced to implement play() capability of Player interface. Since playing tennis is the work for a tennis player, we reused play as work.

Benefits of using Interfaces & Abstract classes-

  • Re-usability: Using inheritance we can design in a way such that code is reused to its maximum. In above example – common activity of a Human like eat is defined in the abstract class.
  • Extensibility – Using interfaces we can design applications that are easily extensible in future. In above example – SoccerPlayer can be added easily by implementing Player and extending Person.

Word of caution: Using Interfaces is somewhat tricky because it makes sense to use them only when we have multiple implementations either currently or in future. If there is no scope of multiple implementation in future then its wiser to avoid interface and use a class directly.

OOP Fundamentals (3 of 3)

Polymorphism is the third essential feature of OOP.

Polymorphism means many forms – an object can behave in different forms and the correct behavior will be evaluated during run-time only.

Upcasting

Taking the concept of inheritance further,  an object of derived class can be used as its own type or as a type of its base class. Although this statement might appear as incorrect – why would Java allow one type to behave like another type, but if we think in a object oriented manner it will make sense.

Inheritance is making a base class (generic) into a more specific derived class. So, derived class object still posses all the generic behavior of base class. By doing upcasting we are loosing specifics of a derived class but retaining generics of base class.

Let’s take this example

upcast

Output :

Car:Move

Although this is the desired output, but looking at the method park() it doesn’t make sense. In the park() method we are invoking move() of Vehicle but actually move() of Car is invoked.

Dynamic Binding

Connecting a method call to method body is called binding. When this binding happens during compile time, its called static binding. When this binding happens during run time (based on type of object), its called dynamic binding or runtime binding.

In the above example, even when Car object is upcasted to Vehicle in park(), it still knows its type information and behave in polymorphic manner to produce desired output.

Downcasting is opposite of upcasting. Here we cast a generic object into more specific one. While Upcasting is always safe and happens implicitly, Downcasting requires explicit type casting operator and throws a runtime ClassCastException if the instance to be downcasted does not belong to the correct subclass.

downcast

 

 

OOP Fundamentals (2 of 3)

Object Oriented Programming Fundamentals

An important aspect of OOP is reuse of classes using composition and inheritance.

Composition is using objects of existing classes into a new class. It represents ‘has-a‘ relationship. e.g. car has-a engine.

composition
Composition (has-a)

Inheritance is creating new classes as type of existing classes. It represents ‘is-a‘ relationship. In Java code, after specifying the name of derived (new) class use the keyword extends followed by name of base (existing) class .

With inheritance derived class gets all the public & protected attributes and methods of base class. There are two more access specifier (other than private and public) –

  • Protected – Like private, protected  entity is accessible to only the Class which defines it. Unlike private, protected entity of base class is available in the derived class.
  • Package – This is the default access where entity is accessible within the package but is private out of package.
inheritance
Inheritance (is-a)

The derived class may add more attribute & methods to the base class. Internally derived class has a object of base class but externally it exposes an extended interface of base class. So,  derived class is a wrapper over base class with some more specific functionality.

Initialization

Since base class is wrapped inside derived class, it becomes responsibility of derived class to do initialization of base class. Java helps here by calling the constructor of base class from the constructor of derived class, but this automatic help is available only in case of default constructors.

When dealing with constructors having arguments, the derived-class constructor needs to explicitly invoke the base-class constructor using super keyword. For example:

Class Furniture {

  Furniture(int i) {

    print(“Creating Furniture”);

  }

}

Class Table extends Furniture {

  Table(int i) {

    super (i);

    print(“Creating Table”);

  }

}

In the above example, constructor Table(int) invokes super(int) which is effectively Furniture(int). A derived class constructor must initialize the base class before doing anything else because initialization of derived may depend on the base class.

OOP Fundamentals (1 of 3)

Object Oriented Programming Fundamentals

OOP is a paradigm to solve real-world business problems by dividing the problem into a set of objects communicating with each other.

An Object consists of attributes and actions to be performed on the attributes. In programming jargon, attributes are called data members and actions are called member functions. Member functions are also referred as methods in Java.

In Java, Class is a type of objects which have same attributes & methods. In other words, a class is definition of attributes and methods. Once we have a class definition or blueprint ready, we can realize the class to create Objects. This realization is called instantiation in programming jargon.

Although Classes are blueprint and Objects are the instances of class, but the term ‘Object‘ is generally used to represent either of them.

Here’s an example of Account class and two of its instances having id 1010 and 1011.

Account class

Encapsulation and Abstraction

A class encapsulates data members and member functions into a single reusable entity. Re-usability of classes a benefit of using OOP; a class once written & tested can be re-used in different scenarios to solve similar business problems.

Abstraction is hiding internal details while exposing only relevant & required information to outside world. In java, abstraction is achieved by use of access specifiers. Here we are introducing only two of four access specifiers in Java –

Public – The entity (class, data member, method) is accessible to everyone.

Private – The entity is accessible to only the Class which defines it.

Let’s take real world example of a Car – which is made-up of a lot of attributes like engine, steering, brake pedal, gear-box, tyres, brake shoe, axle, etc. Car exposes interface to the user (methods) like changing gear, applying brake, etc. without telling the user that there is a brake shoe. Someone driving a car does not need to know that it has brake shoe but they need to know how to apply brake.

The benefit to using abstraction is that Car manufacturer can actually switch a mechanical brake with a hydraulic brake without any impact on the way people use or apply brake.

A software world example can be a AlarmLogger class – whose job is to write alarms in a file using a specific format. This class exposes a public method logAlarm and hides internal file-handing and formatting from clients calling this class by keeping them as private. This same class can be reused in multiple Programs like ecommerce app, banking app.