This post can be seen here as well.
The main idea from this article is to show the SOLID design principles and provide examples of implementations of those principles using Java as the main language.
What is SOLID?
S.O.L.I.D stands for:
Single Responsibility Principle
Interface Segregation Principle
Dependency Inversion Principle
Design principles in general helps us to write better software. And also improves the developer experience of the developers that share the same codebase with you.
Single Responsibility Principle
A class should have only one responsibility.
It helps into onboarding new members into the code, as well to test, maintain and grow our codebase.
Example
Imagine that you have a UserService
that is like this:
class UserService {
public void createUser() {
// Create our user here
}
public User findById(UUID id) {
// Return our user
}
private void sendAppNotification() {
// Send an app notification
}
private void sendEmail() {
// Send an email
}
}
What happens if the requirements of this class change? What if now we must send a tex now we have to send an App Notification via push or SMS?
Then, we must separate the concerns and responsability by creating a NotificationService
that will handle all of our communications.
class UserService {
private final NotificationService notificationService;
public void createUser() {
// Create our user here
}
public User findById(UUID id) {
// Return our user
}
}
class NotificationService {
public void sendSms() {
// Send SMS
}
public void sendPushNotification() {
// Send push
}
public void sendEmail() {
// Send email
}
}
Even though it can have better approaches here, the idea is to separate concerns, the other solutions to solve this kind of problem is a different topic.
Open Closed Principle
Software entities (classes, modules, functions, etc.) should be opened to extension but closed to modification.
Example
Think that we have an CoffeeApp class
public class CoffeeApp {
public void brewSimpleCoffee() {
// Put water
// Put coffee powder
}
public void brewPremiumCoffee() {
// Get the bean
// Grind the bean
// Put water
}
}
Imagine that everytime our system needs to support a new type of coffee, we must change the Machine in order to add a new type.
Lets use of simple abstraction and polymorphism to improve this
public class CoffeeApp {
public static void greet(CoffeeMachine coffeeMachine) {
coffeeMachine.brewCoffee(ESPRESSO);
}
}
interface CoffeeMachine {
Coffee brewCoffee(CoffeeSelection selection);
}
public class BasicCoffeeMachine implements CoffeeMachine {
private BrewingUnit brewingUnit;
@Override
public Coffee brewCoffee(CoffeeSelection selection) {
brewFilterCoffee();
}
private brewFilterCoffee() {
brewingUnit.brew();
}
}
public class PremiumCoffeeMachine implements CoffeeMachine {
private Grinder grinder;
private BrewingUnit brewingUnit;
@Override
public Coffee brewCoffee(CoffeeSelection selection) {
switch(selection) {
case ESPRESSO:
return brewEspresso();
case FILTER_COFFEE:
default:
return brewFilterCoffee();
}
}
private brewEspresso() {
grinder.grind()
brewingUnit.brew();
}
private brewFilterCoffee() {
brewingUnit.brew();
}
}
Now that class is opened for extension (ItalianMachine, ColombianMachine, FrenchPressMachine) and closed for modification (won’t have a method with a different logic being added every time a new machine is added to the app).
Liskov Substitution Principle
A class can be replaced by its subclass in all practical usage scenarios, meaning that you should use inheritance only for substitutability.
Example
Using an Animal example
interface Animal {
void fly();
void swim();
}
public class Dog implements Animal {
// A dog can swim
@Override
public void swim() {
// Swim
}
// But a dog cannot fly
@Override
public void fly() {
throw new IllegalStateException();
}
}
public class Hawk implements Animal {
// A hawk cannot swim
@Override
public void swim() {
throw new IllegalStateException();
}
// But a hawk can fly
@Override
public void fly() {
// Fly
}
}
Even though, both a Dog
and a Hawk
are animals, we can break up the inheritance to follow up this principle
interface Animal {
void swim();
}
interface Bird {
void fly();
}
public class Dog implements Animal {
// A dog can swim
@Override
public void swim() {
// Swim
}
}
public class Hawk implements Bird {
// A hawk can fly
@Override
public void fly() {
// Fly
}
}
Interface Segregation Principle
A client shouldn’t be forced to implement an interface that it doesn’t use.
Example
Thinking about the last example, even though a Dog can only swim, some Animal
s can swim and fly.
Isn't easier if we just implement the interfaces like this
interface Swimmer {
void swim();
}
interface Bird {
void fly();
}
public class Dog implements Swimmer {
// A dog can swim
@Override
public void swim() {
// Swim
}
}
public class Hawk implements Bird {
// A hawk can fly
@Override
public void fly() {
// Fly
}
}
public class Duck implements Swimmer, Bird {
// A duck can fly
@Override
public void fly() {
// Fly
}
// A duck can swim
@Override
public void swim() {
// Swim
}
}
Dependency Inversion Principle
We should invert the classic dependency between higher level modules and lower level modules, by abstracting their interaction.
Example
Let's say we have some implementations over a database
public class PersonService {
private final PersonRepository personRepository;
public PersonService(PersonRepository personRepository) {
this.personRepository = personRepository;
}
}
interface PersonRepository {
Person findById(UUID id);
Person create(UUID id, String name);
}
public class LocalRepository implements PersonRepository {
private Map<UUID, Person> repository;
// Implement the methods
}
public class DatabaseRepository implements PersonRepository {
// Hibernate entity manager to handle the communication towards the Database.
private EntityManager entityManager;
// Implement the methods
}
In this way, the high-level PersonService doesn't care if you are using a LocalDatabase for development or a real database for production.
And for example, you can change your Hibernate implementation to another solution without your high-level service knowing what's going on.
In case if you have any questions or suggestions, feel free to send me a message
Top comments (1)
Another way to phrase the Single Responsibility Principle is Uncle Bob's "a class should have only one reason to change".
Your example illustrates it very well: the original class could change for many reasons (a change in the requirements for the User entity, or in the notification methods) while the refactored classes each can change only because something in their domain changes.