post-images/solid-principles/cover.jpg

SOLID Principles

Hello, today I will talk to you about SOLID principles, one of the most important principles accepted in the software world. Although SOLID actually means solid on its own, I cannot say it is directly related to our field of interest. It is just an abbreviation formed by the initials of a few principles and is easier to remember 🤷🏻‍♂️. This is one of the reasons why I wrote this article, in fact, I can forget their names after a while, no matter how much I use them while developing. I hope it will be a good note for both you and me...

I can't say anything about the SOLID principles being neither new nor old. It was coined by our Uncle Bob (Robert C. Martin, also known as [Uncle Bob] (http://en.wikipedia.org/wiki/Robert_Cecil_Martin)) about 22 years ago in 2000. Thinking of you and me, our uncle published these principles to help us write better code. It has paved the way for us to write more sustainable, understandable, flexible, reusable code, to get rid of code smeller, to do refactoring, to develop agile and adaptive software.

What a perfect recipe, right? The description of developing an application in the above way is very simple, first of all, I will give a short summary of what SOLID is and then I will go into the details.

TL;DR

  • Single Responsibility: An object or method should have only one responsibility.
  • Open/Closed: Our object or method should be open to development but closed to modification.
  • Liskov Substitution: In the event that the child object substitution the parent, it must be able to use all the properties of the parent class.
  • Interface Segregation: Responsibilities need to be broken down into smaller meaningful parts, not in a single interface.
  • Dependency Inversion: Dependencies must be managed through an abstraction instead of a class not being directly dependent on another class.

What do you think, it is understood what they mean partially, if not completely, from their names? No? Doesn't make sense to you or if you want to fully grasp it. Let me start with my first item for you and reinforce it with some examples. We'll discuss it if necessary.

Single Responsibility

Single Responsibility argues that in the ideal world everyone has only one responsibility and should only deal with it. So if you are a software engineer, you are expected to develop software. If you go and design construction, that will make it difficult to identify you. When you try to do both at the same time, it causes you to not be able to do the main job with full performance and the environment is dragged into chaos. A class or method you write with this approach should only do what it's supposed to do. In any case, if an action is defined in a method or class other than the reason for its creation, this will break the single responsibility.

public class Blogger {
    @Override
    public Blog getBlogById(Long id) {
        return null;
    }
    @Override
    public void addBlog(Blog blog) {
	...
    }
    @Override
    public void sendEmail(Blog blog, User recipient) {}
}

I think the class looks pretty nice, are there any problems? No need to ask, it's obvious, what do you say? What does the act of sending mail in a blog-related class have to do with it? So what method would be right to follow?

public class Blogger {
    @Override
    public Blog getBlogById(Long id) {
        return null;
    }

    @Override
    public void addBlog(Blog blog) {}
}
public interface EmailSender {
    public void sendEmail(Blog blog, User recipient);
}

I have separated the responsibilities of adding a blog and sending mail about this blog. In this way, I have established a more sustainable, flexible, organized and reusable structure. When I want to add something extra to the mailing flow, I will not add these requirements to the Blogger class and drag my class into chaos.

Open/Closed

I think it is the principle that is easiest to explain and understand. In the clearest definition, our object or method should be open to development but closed to modification. Someone else may have used the class before. After the new development I have made, there should be no need for any changes in the usage areas. Existing improvements should not be changed, but new features should be added.

I found a good example on Baeldun - Open/Closed Principle in Java and I will explain it through this example.

public  interface  CalculatorOperation {}

For the addition process;

public class Addition implements CalculatorOperation {
    private double left;
    private double right;
    private double result = 0.0;

    public Addition(double left, double right) {
        this.left = left;
        this.right = right;
    }
    ...
}

For the subtraction process;

public class Subtraction implements CalculatorOperation {
    private double left;
    private double right;
    private double result = 0.0;

    public Subtraction(double left, double right) {
        this.left = left;
        this.right = right;
    }
    ...
}
public class Calculator {

    public void calculate(CalculatorOperation operation) {
        if (operation == null) {
            throw new InvalidParameterException("Can not perform operation");
        }

        if (operation instanceof Addition) {
            Addition addition = (Addition) operation;
            addition.setResult(addition.getLeft() + addition.getRight());
        } else if (operation instanceof Subtraction) {
            Subtraction subtraction = (Subtraction) operation;
            subtraction.setResult(subtraction.getLeft() - subtraction.getRight());
        }
    }
}

We have built a calculator using the classes we have. How do you think it happened? Since we are processing the Open/Closed Principle (OCP), a situation that breaks OCP must have caught your eye here. If I had to help a little, for example, if I needed division besides addition and subtraction, what should I do? I need to write a new division class and then add a condition like else if (operation instanceof Subtraction) in Calculator, right? It's okay for us to write a division class, but changing the already written calculate method is against OCP. Instead, I could set up a structure like this.

We have created a common method to use in our operations, and we will fill this method with polymorphism in transactions.

public  interface  CalculatorOperation { 
    void  perform(); 
}

We created our class that will do the addition and applied the perform method to perform the addition.

public class Addition implements CalculatorOperation {
    private double left;
    private double right;
    private double result;

    ...

    @Override
    public void perform() {
        result = left + right;
    }
}

In the same way, we created our class to perform the division and applied the perform method to perform the division operation.

public class Division implements CalculatorOperation {
    private double left;
    private double right;
    private double result;

    ...

    @Override
    public void perform() {
        if (right != 0) {
            result = left / right;
        }
    }
}

If we need to edit our Calculate class;

public class Calculator {

    public void calculate(CalculatorOperation operation) {
        if (operation == null) {
            throw new InvalidParameterException("Cannot perform operation");
        }
        operation.perform();
    }
}

Now we can add as many additions, subtractions, multiplications and divisions as you want to our system without any changes.

We did not allow changes and did not break the expansion feature. Perfect 👌🏼

Liskov Substitution

This principle, which is referred to as Liskov's substitution principle, basically advocates the full fulfillment of tasks during substitutions. The substitution here may not have meant anything to you at this stage. The mentioned substitution; When a child object is replaced by a parent, it must use all the properties of the parent class. If we use neither, we break the principle of Liskov Substitution.

Suppose we have a bird class and we have methods that perform the eating and flying actions of the bird we represent. If we use this Bird class that we have written;

public class Bird{
    public void eat() {}
    public void fly() {}
}
public class Eagle extends Bird{}

The eagle is a bird and now we can call the Eagle.eat() or Eagle.fly() methods. But for the penguin, which is a flightless bird, the eat() method would be logical, but fly() would not be used. Because the penguin is a flightless bird.

public class Penguin extends Bird{} 

Since the fly() method will be overridden and not used, you will probably need to throw an exception in it. It is against the Liskov Substitution principle that the subclass (Penguin) does not use all the methods of the superclass (Bird).

How could I do this without breaking the principle? We need to add one more abstraction layer in between. Namely:

public class Bird{
    public void eat(){}
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Eagle extends FlyingBirds{}
public class Penguin extends Bird{} 

When the subclass and superclass are swapped by using a structure as above, the subclass will completely use the superclass's methods and meet the requirements of our principle.

If I need to reinforce with another good example, we can have the opportunity to write the subject more flexible with a 'Scan' layer from time to time, instead of a structure that has both 'print()' and 'scan()' methods in 'BasePrinter'.

abstract class BasePrinter {
    abstract public function print();
}
interface Scan{
    public funtion scan();
}
class HpPrinter extends BasePrinter  {
    public function print(){}
}
class CanonPrinter extends BasePrinter implements Scan {
    public function print() {}
    public function scan() {}
}

Interface Segregation

Within the scope of this principle, the phrase "forced beauty" always comes to my mind. So imagine an interface and assume that all the responsibilities are in this interface, if I implement this interface in a small class to use only a few methods, I will have to override all the remaining methods and these methods will pollute my class unnecessarily. Instead, it is necessary to divide interfaces into more meaningful parts, avoiding unnecessary methods. In summary, this principle proposes not to assign responsibilities to a single interface, but to divide them by customized meaningful interfaces. I may not have been able to reflect the concept of size when I said interface with all methods, small class, and I want to reinforce it with an example.

For example, let's take the Printer example in the previous example again;

public interface PrinterTasks {
    void print(String printContent);
    void scan(String scanContent);
    void fax(String faxContent);
    void printDuplex(String printDuplexContent);
}

Let's assume that we have an interface that hosts processes belonging to the printer. In this case, there will be no problem for the full-featured HPLaserJetPrinter implementation:

public class HPLaserJetPrinter extends PrinterTasks {
    public void print(String printContent) {}
    public void scan(String scanContent) {}
    public void fax(String faxContent) {}
    public void printDuplex(String printDuplexContent) {}
}

But for LiquidInkjetPrinter model printer we will have fax() and printDuplex() methods which are not used:

public class LiquidInkjetPrinter extends PrinterTasks {
    public void print(String printContent) {}
    public void scan(String scanContent) {}

    public void fax(String faxContent) {
        throw new UnsupportedOperationException();
    }

    public void printDuplex(String printDuplexContent) {
        throw new UnsupportedOperationException();
    }
}

As you can see, we have overridden unused methods unnecessarily. This situation broke the Interface Segregation principle. To solve this problem, it would be logical to divide the non-common features into different interfaces. Namely;

public interface PrinterTasks {
    void print(String printContent);
    void scan(String scanContent);
}
public interface FaxTasks {
    void fax(String faxContent);
}
public interface PrintDuplexTasks {
    void printDuplex(String printDuplexContent);
}

The division above seems perfectly logical. When we examine their use...

public class HPLaserJetPrinter extends PrinterTasks, FaxTasks, PrintDuplexTasks {
    public void print(String printContent) {}
    public void scan(String scanContent) {}
    public void fax(String faxContent) {}
    public void printDuplex(String printDuplexContent) {}
}
public class LiquidInkjetPrinter extends PrinterTasks {
    public void print(String printContent) {}
    public void scan(String scanContent) {}
}

As you can see, each class implements the abstraction, which has its own characteristics, so that other dependencies are not unnecessarily forced.

Dependency Inversion

It argues that a class should not be directly linked to another class and that dependencies should be provided through abstraction. Abstract objects should not depend on concrete objects, but concrete objects should depend on abstract objects, and also high-level modules should not receive anything from low-level modules. Both must depend on the abstraction. I'm going to the example right away, I understand better with the example 😅

Let's have a BE developer:

public class BackEndDeveloper {
    public void writeJava() {}
}

Let's also have one FE developer:

public class FrontEndDeveloper {
    public void writeJavascript() {}
}

We have two developer and if we want these two developers to work on a project:

public class Project {
    private BackEndDeveloper backEndDeveloper = new BackEndDeveloper();
    private FrontEndDeveloper frontEndDeveloper = new FrontEndDeveloper();

    public void implement() {
        backEndDeveloper.writeJava();
        frontEndDeveloper.writeJavascript();
    }
}

It looks pretty stylish. So is it? For example, what will you do when we ask to get one more BE developer involved in the project? What if we add a FullStack developer to the project?

public class Project {
    private BackEndDeveloper backEndDeveloper = new BackEndDeveloper();
    private BackEndDeveloper backEndDeveloper2 = new BackEndDeveloper();
    private FrontEndDeveloper frontEndDeveloper = new FrontEndDeveloper();
    private FullStackDeveloper fullStackDeveloper = new FullStackDeveloper();

    public void implement() {
        backEndDeveloper.writeJava();
        backEndDeveloper2.writeJava();
        frontEndDeveloper.writeJavascript();
        fullStackDeveloper.writeJava();
        fullStackDeveloper.writeJavascript();
    }
}

You understand where the situation arises and why we should care about Dependency Inversion. There is a very easy way to solve this problem. First of all, let me review the structure I have set up. All of the people involved in the project are developers. Why shouldn't I go to an abstraction for developers?

public interface Developer {
    void develop();
}
public class BackEndDeveloper implements Developer {
    @Override
    public void develop() {
        writeJava();
    }
    private void writeJava() {}
}
public class FrontEndDeveloper implements Developer {
    @Override
    public void develop() {
        writeJavascript();
    }

    public void writeJavascript() {}
}
public class Project {
    private List<Developer> developers;
    public Project(List<Developer> developers) {
        this.developers = developers;
    }
    public void implement() {
        developers.forEach(d->d.develop());
    }
}

Now you can add as many developers as you want to the project and you don't need to touch anything. Isn't it super?

Summary

How important are the SOLID principles? In fact, each of them has a logical reason for its emergence. I do not look at the principles set for a standardization, but rather as the elements required to improve the quality of the written codes. I hope it worked for you and you will remember and apply it many times in your daily life.

Addio🙋🏻‍♂️

SOLIDOOPJavaSingle ResponsibilityOpen/ClosedLiskov SubstitutionInterface SegregationDependency Inversion