Alex Sikorski
Sep 02, 2022
#swe
#java
SOLID stands for:
This principle is self-explanatory. One class should have one and only one responsibility. In practice, the benefits are; less test cases per class, lower coupling and therefore less functionality in one class which leads to fewer dependencies and better organization, easier to navigate behaviours in a specified class.
For example, if we have a Book
class with the state name
, author
, content
and with the method (behaviour) printBook
. This class violates this principle, as a Book should not have to print itself. In this case, a new class would be introduced, called Printer
. This class would then be used to print the Book's contents.
class Book
{
String name;
String author;
String content;
Book(String name, String author, String content){
this.name = name;
this.author = author;
this.content = content;
}
private void printBook(){
// violation! introduce new Printer class with printing responsibility.
}
}
This principle states that software components should be open for extension, but closed for modification. As a developer, if you wanted to edit behaviour, you would not change the existing code but extend a class and then override a method. This simply prevents the introduction of code that can lead to new bugs.
For example, if we have a Guitar
class with the state make
, model
, and we wanted to give it some awesome flame decals, we would not edit the existing Guitar
class, but we would extend
Guitar
with a new class GuitarWithFlames
and add the state flameColour
.
class Guitar
{
String make = "Fender";
String model = "Stratocaster";
}
class GuitarWithFlames extends Guitar
{
String flameColour = "Red";
}
This principle states that any derived class should be able to substitute its parent class. So, the behaviour must be consistent in the parent and child class.
For example, let's say we have a class Bird
with the method fly()
. A Pigeon
can extend this class since it can fly, however if an Ostrich
extended it, it would fail the Liskov test since the Ostrich
cannot fly.
A fix for this would require a new class called FlyingBirds
that extends Birds
. The result would look like:
public class Bird{}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
This principle states that larger interfaces should be split up into smaller, specific ones. This effectively ensures that classes aren't forced to depend on methods that will not be used.
For example, let's say a BearKeeper
interface has the following behaviours washBear()
, feedBear()
, petBear()
. This interface can be split into a BearCleaner
, BearFeeder
and a BearPetter
. In this example, only a CrazyPerson
would implement the BearPetter
whilst a BearCarer
would implement BearCleaner
and BearFeeder
.
// from
interface BearKeeper
{
void washBear();
void feedBear();
void petBear();
}
// to
interface BearCleaner
{
void washBear();
}
interface BearFeeder
{
void feedBear();
}
interface BearPetter
{
void petBear();
}
// the implementation
class CrazyPerson implements BearPetter
{
@Override
void petBear(){
// because I am crazy
}
}
class BearKeeper implements BearCleaner, BearFeeder
{
@Override
void washBear(){}
@Override
void feedBear(){}
}
This principle states: depend on abstractions, not on concretions. High level modules should not depend on low level modules. Both should depend on abstractions.
For example, let's say we have a RemoteControl
that has a Television
instance and the RemoteControl
itself can click()
. The Television
can turnOn()
or turnOff()
. The click()
method has the logic for turning the Television
on or off.
This works fine, but there is room for improvement. With abstraction, we can make sure that the RemoteControl
can control anything with a turnOn()
or turnOff()
button (e.g. a light, a computer or speaker). An interface can be created, called OnOffDevice
that contains methods turnOn()
and turnOff()
. Now, our RemoteControl
example will depend on the new OnOffDevice
abstraction as opposed to the concrete Television
class. The Television
class now also depends on the OnOffDevice
with its method implementations for turning on and off.
// from
class RemoteControl
{
Television tv; // instance
void click(){
tv.turnOn();
tv.turnOff();
}
}
// to
class RemoteControl
{
OnOffDevice device; // instance
void click(){
device.turnOn();
device.turnOff();
}
}
// where
class Television implements OnOffDevice
{
@Override
turnOn(){
// turn on
}
@Override
turnOff(){
// turn off
}
}
Effectively, this frees high-level components from being dependent on details of low-level components, and it helps design software that is reusable and resilient to changes.
Currently working as a full stack Software Engineer and curiously seeking new knowledge in free time.