SOLID

WHAT ARE THE SOLID PRINCIPLES?

The SOLID Principles are the five basic principles that tell you how to write good object-oriented code. They were proposed by the famous American programmer Robert Martin. He is also one of the makers of the Agile Manifesto.

“S” – Single Responsibility Principle

By definition, this is simply the principle of single responsibility: Each module, class, or function in a computer program should account for and contain a single portion of that program’s functionality. All services of this module, class or function should be closely related to this responsibility. Let’s analyze the following code that does not respect this rule:

class Person
{
public String Name {get; set; }
public String Surname {get; set; }
public String Town {get; set; }
public String Street {get; set; }
public int HouseNumber {get; set; }

public Person (String name, String surname, String town)
{
Name = name;
Surname = surname;
Town = town;
}
}

The above class contains a constructor where in order to create an object of the Person class, we must also specify the location – this should not be in Person type obligation. Is there anything else that can be improved ? – Yes. The Person class should not contain attributes that are not related to it, such as town, street or house number. In the future, someone will have to refactor this class or duplicate the code in order to keep the address itself isolated. It will be better to distinguish two classes here:

class Address
{
public String Town {get; set; }
public String Street {get; set; }
public int HouseNumber {get; set; }
}
class Person
{
public String Name {get; set; }
public String Surname {get; set; }
public Address PersonalAddress {get; set; }

public Person (String name, String surname)
{
Name = name;
Surname = surname;
}
}

Following the principle of single responsibility, we separate the address fields into a separate class, as a result each class now only solves one problem, and changing one does not necessitate making changes to the other class. Don’t get the feeling of chipping too much code is bad. Even in such a trivial case as the class Person, it is better to extract the fields related to the address into a separate class.

“O” – Open / Close Principle

Programming entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This principle should always be developed into “open to extension, closed for modification”. It is practically all and complete definition. This is a very important rule, especially in large projects that many developers work on. Each class should be written in such a way that it is possible to add new functionalities without having to modify it. Modification is strictly forbidden because changing the declaration of any method could crash the system elsewhere. This rule is especially important for developers of plugins and programming libraries. There is a certain dependence, the more we stick to the principle of single responsibility, the more we have to adhere to open to extension & closed for modification principle. Consider an example:

public class Greeter {

String typeOfGreeting;

public String greet() {

if (this.typeOfGreeting == "formal") {
return "Hello";
}
else if (this.typeOfGreeting == "friendly") {
return "Hello brother";
}
else if (this.typeOfGreeting == "romantic") {
return "Hello honey";
}
else {
return "Hello";
}
}

public void setTypeOfGreeting (String typeOfGreeting) {
this.typeOfGreeting = typeOfGreeting;
}
}

public class SendGreeting {

public static void main (String [] args) {

Greeter greeter = new Greeter();
greeter.setTypeOfGreeting("formal");
System.out.println (greeter.greet());
}
}

In the example above, adding a new type of greeting to the Greeter class requires modification of that class. This is breaking the open / closed rule because the class is not open to expansion. We have to modify this class each time by adding a new condition. Now consider the following code, specifically the Greeter class:

public interface Greeting {
String greet();
}

public class Greeter {

private Greeting greeting;

public Greeter (Greeting greeting) {
this.greeting = greeting;
}

public String greeting() {
return this.greeting.greet();
}
}

public class FormalGreeting implements Greeting {

public String greet() {
return "Hello";
}
}

public class FriendlyGreeting implements Greeting {

public String greet() {
return "Hello brother";
}
}

public class RomanticGreeting implements Greeting {

public String greet() {
return "Hello honey!";
}
}

public class SendGreeting {

public static void main (String [] args) {

Greeter greeter = new Greeter(new FormalGreeting ());
System.out.println (greeter.greeting());
}
}

The Greeter class in this case has simplified a lot. Note that now, to add another type of greeting, we don’t need to change anything at all in this class. We can just add another class to represent a new type of greeting. In this way, we will extend/expand our applications with another functionality but we do not modify the existing one.

“L” – Liskov Substitution Principle

The rule is that subclasses should meet the expectations of clients accessing objects in subclasses through references of superclass types not just as regards syntactic safety (such as absence of “method-not-found-errors”), but also in terms of behavioral correctness. If we have the base class Animal, after which the two classes inherit: Dog and Cat, then any function that takes the Animal type in parameter, should also handle the Dog and Cat instances. If additional conditions are needed to be checked, or worse, an exception to be thrown, depending on class type, it is breaking Liskov rule. Let’s look at the example below:

abstract class Animal {
public abstract void setName ();
public abstract void run();
}

public class Dog extends Animal {

@Override
public void setName() { System.out.println ("Dog Reksio"); }

@Override
public void run() { System.out.println ("Reksio is running"); }
}

public class Cat extends Animal {

@Override
public void setName () {System.out.println ("Cat Frisky"); }

@Override
public void run() {System.out.println ("Frisky is running"); }
}

public class Fish extends Animal {

@Override
public void setName () { System.out.println ("Golden Fish"); }

@Override
public void run () {
throw new ExecutionControl.NotImplementedException ("The fish can not run!"); }
}

public class Pets {

public static void main (String [] args) {
List <Animal> animals = new ArrayList <Animal> ();
animals.add (new Dog ());
animals.add (new Cat ());
animals.add (new Fish ());

animals.forEach (Animal :: setName); // works for all classes in the list
animals.forEach (Animal :: run); // won't work for fish
}
}

In the above example, we have created the Animal abstract, but there is a phenomenon of a poorly thought-out mechanism of inheritance here. The fish is animal, but was burdened with the implementation of the run() method in the base class. A fish like a fish can’t run and that’s it, in this case it is breaking the Liskov substitution principle. Inheritance should be planned differently so that each derived class can use the base class functions. So let’s write the classes according to the following scheme:

abstract class Animal {
abstract void setName();
}

interface WalkingAnimals {
void run();
}

interface SwimmingAnimals {
void swim();
}

public class Dog extends Animal implements WalkingAnimals {

@Override
public void setName() {
System.out.println ("Dog Reksio");
}

@Override
public void run() {
System.out.println ("Reksio is running");
}
}

public class Cat extends Animal implements WalkingAnimals {

@Override
public void setName() {
System.out.println ("Frisky Cat");
}

@Override
public void run() {
System.out.println ("Frisky is running");
}
}

public class Fish extends Animal implements SwimmingAnimals {

@Override
public void setName() {
System.out.println ("Golden Fish");
}

@Override
public void swim() {
System.out.println ("The Golden Fish is swimming");
}
}

public class Pets {

public static void main (String [] args) {

List <Animal> animals = new ArrayList <Animal> ();
animals.add (new Dog ());
animals.add (new Cat ());
animals.add (new Fish ());
animals.forEach (Animal :: setName); // works for all classes in the Animal list

List <WalkingAnimals> walkingAnimals = new ArrayList <WalkingAnimals> ();
walkingAnimals.add (new Pies ());
walkingAnimals.add (new Cat ());
walkingAnimals.forEach (WalkingAnimals :: run); // works for all classes of the 'WalkingAnimals' list type
}
}

In this situation, we have distinguished common features into separate interfaces that can be used when needed and we avoid situations where some child class inherits a method that cannot be executed by it.

PRECONDITIONS in Liskov principle

The preconditions for each subclass cannot be strengthen (more demanding) than the preconditions of the parent class. They may, however, be weaker (less demanding) than the preconditions of the parent class. Liskov says – any derived class can be used in place of the base class (all methods are compatible). If we have a base class Parent, from which the class: Child inherits, then any function with the type of Parent in the parameter should also handle the Child instance. If additional checks of conditions are needed, or worse, an exception is thrown depending on the class type, this is a breach of Liskov’s rule. Let’s look at the example below:

public class Parent {

public double calculateBookPrice (String title, double bookPrice) throws IOexception {

if (title.isEmpty()) {
throw new IOException();
}

double bookPriceWithDelivery = bookPrice + 5;
return bookPriceWithDelivery;
}
}

public class Test {

public static void main (String [] args) {
double sellingPrice = new Parent(). calculateBookPrice (“Book”, 20);
System.out.println("sellingPrice");
}
}

If we create a Child class that inherits from the Parent class, and change the inherited Precondition to be more difficult to fulfill, or in other words – if we make this condition more stringent, we notice that for objects of the Child class, given name of the book beginning with a letter “B”, it will throw an exception, however for the parent class object, this exception will not happen. This prevents the use of a Child class for the same method of a Parent type:

public class Child extends Parent {

public double calculateBookPrice (String title, double bookPrice) throws IOexception {

if (title.isEmpty () || title.startsWith ("B")) {
throw new IOException();
}
double bookPriceWithDelivery = bookPrice + 5;
return bookPriceWithDelivery;
}
}

public class Test {

public static void main (String [] args) {

double sellingPrice = new Parent(). calculateBookPrice (“Book”, 20); // with no exception thrown here
double sellingPrice = new Child(). calculateBookPrice (“Book”, 20); // exception is thrown here
System.out.println (sellingPrice);
}
}

Such a situation is not desirable because for the same methods we should be able to use objects of both classes interchangeably. If, on the other hand in the Child class, the precondition was weaker, i.e. we reduced its requirements, this behavior would be allowed – as below:

public class Parent {

public double calculateBookPrice (String title, double bookPrice) throws IOexception {

if (title.isEmpty () || title.startsWith ("B"))) {
throw new IOException ();
}
double bookPriceWithDelivery = bookPrice + 5;
return bookPriceWithDelivery;
}
}

public class Child extends Parent {

public double calculateBookPrice (String title, double bookPrice) throws IOexception {

if (title.isEmpty ()) {
throw new IOException();
}

double bookPriceWithDelivery = book price + 5;
return bookPriceWithDelivery;
}
}

POSTCONDITIONS in Liskov principle

The opposite is true for the end conditions in subclasses, namely: The post conditions of each subclass cannot be weaker (less stringent) than the post conditions of the Parent class. They may, however, be stronger (more demanding) than the post conditions of the Parent class.

In a well-planned inheritance mechanism, derived classes should not override the methods of the base classes. They can possibly expand them, by calling a method from the base class (e.g. through a keyword that is a pointer to the base class).

“I” – Interface Segregation Principle

The interface segregation rule is simple – it says not to create interfaces with methods that some class implementing this interface will not use. Interfaces should be concrete and as small as possible. Usually it is better to use an abstract class to create a base type. It can describe a specific type, contain the appropriate attributes and methods which are then burdened with all derived classes. The base class defines the business model we need. The interface, on the other hand is stateless, it should not define a business model. The interface should provide a contract that informs the developer of the behavior type. Sample code:

public interface Player {

void preStartWarmUp ();
void swimming ();
void highJump ();
void poleVault ();
}

public class MichalKaras implements Player {

@Override
public void preStartWarmUp() {
System.out.println ("Michal started his warm-up before the start");
}

@Override
public void swimming() {
System.out.println ("Michal started the swim competition");
}

@Override
public void highJump() {
}

@Override
public void poleVault(){
}
}

As we can see, Michał is a swimmer and he will not need methods to handle the pole vault or the high jump. Therefore it will be better to split the main Player interface and extract individual competitions into separate interfaces. The main problem in the example above is the fact that we use a general “fat” interface, instead of smaller ones with a responsibility limited to a specific task. Taking into account the interface segregation principle we could write our code as below:

public interface Player {
void preStartWarmUp ();
}

public interface SwimCompetition {
void swimming ();
}

public interface HighJumpCompetition {
void highJump ();
}

public interface PoleVaultCompetition {
void poleVault ();
}

public class MichalKaras implements Player, SwimCompetition {

@Override
public void preStartWarmUp () {
System.out.println ("Michal started his warm-up before the start");
}

@Override
public void swimming () {
System.out.println ("Michal started the swim competition");
}
}

By dividing the interface into smaller ones, we maintain order in the polymorphic interface of the type. This keeps the derived types in unrelated contracts they don’t need. Breaking the principle of interface segregation leads to a situation when iterating through a list of base types with a common polymorphic interface, an exception is thrown because one of the classes does not implement the rich interface method.

“D” – Dependency Inversion Principle

High level modules should not depend on low level modules. Both should depend on abstraction. All dependencies should depend on abstraction as much as possible and not on a specific type. Let’s look at the example below:

public class BackEndDeveloper () {

public void startProgramming() {
System.out.println ("PHP developer in action ...");
}
}

public class FrontEndDeveloper () {

public void startProgramming () {
System.out.println ("JavaScript developer in action ...");
}
}

public class Project () {

private BackEndDeveloper backEndDeveloper;
private FrontEndDeveloper frontEndDeveloper;

public Project () {
this.backEndDeveloper = new BackEndDeveloper ();
this.frontEndDeveloper = new FrontEndDeveloper ();
}

public void startProject () {
backEndDeveloper.startProgramming ();
frontEndDeveloper.startProgramming ();
}
}

The above code is not optimal as the Project class directly depends on developers objects. Much better to introduce an additional abstract type to be based on something more general. Thanks to this, we reduce the link between the classes (tight coupling) which is the main purpose of applying this rule. We could write down:

public interface Developer {

void startProgramming();
}

public class BackEndDeveloper implements Developer {

@Overrite
void startProgramming () {
programInPHP ();
}

private void programInPHP () {
System.out.println ("PHP developer in action ...");
}
}

public class FrontEndDeveloper implements Developer {

@Overrite
void startProgramming () {
programInJavaScript ();
}

private void programInJavaScript () {
System.out.println ("JavaScript developer in action ...");
}
}

public class Project {

private List <Developer> developers;

public Project (List <Developer> developers) {
this.developers = developers;
}

public void startProject () {
developers.forEach (
developer -> developer.startProgramming ()
);
}
}

The most important thing to remember is that dependency inversion helps us to work more on abstraction.

Summary

When writing code by yourself, you generally understand everything, even if the code is of poor quality. Everyone simply understands what he wrote himself. The real problem comes when a stranger is forced to switch to a project of someone and write in code that he is not familiar with… Then it helps a lot that:

  • Instead of one class with 2000 lines of code, there are 20 small classes, each of which is responsible for one small, specific thing (principle of single responsibility)
  • The author of the class foresaw extending the functionality of his class without the need to modify its code, e.g. through the mechanism of inheritance and polymorphism (the open / closed principle)
  • By using derived classes, we are sure that they implement all the methods of the base classes and we do not have to check it (the liskov substitution principle)
  • The system is made of small interfaces (often with only one method), thanks to which we are able to implement in the newly added class only 2 interfaces that we need and no more, without unnecessary methods (interface segregation)
  • The previous programmer used abstract types wherever possible (e.g. in function parameters) so we can pass to the function any collection that implements the Programmer interface, not just a list. And the interfaces in the project make sense and can be used, and they are not just art for art, because why do we need interfaces, if all functions take derived types as parameters (dependency inversion)

Hope you will find it easier to understand SOLID principles after reading this article. This is a good step towards writing cleaner code.

Leave a Comment

Your email address will not be published. Required fields are marked *