SOLID Principles
by Ronni Kahalani, Copenhagen School of Design & Technology.
The 5 best practice software quality habits.
Who am I?
Thank you for stopping by.
I’m Ronni. I hope you’re well and wish you a safe and worthy journey.
This presentation is part of the Software Engineering Series, from my lectures at Copenhagen School of Design & Technology.
You can view the Introducing Myself, if you want to know a little more about who I am.
All my presentations and materials are free and available at my blog post: Software Engineering.
Don’t let me uphold you,
continue your journey, go to next slide.
2
Agenda
3
5 SOLID Principles
Bad code design foster inflexible and brittle solutions, which de-motivate developers. Small changes in software can result in major bugs and consequences.
SOLID is the foundation for great testable code.
A Test Driven Development (TDD) mindset, based on 5 principles:
4
Single Responsibility (SR)�Open-Closed (OC)�Liskov Substitution (LS)�Interface Segregation (IS)�Dependency Inversion (DI)
Single Responsibility Principle
A class should serve only one purpose.
6
Single Responsibility Principle (Definition)
A class serving multiple purposes or responsibilities, should be split into multiple classes.
Goals
7
Single Responsibility Principle (Violation)
Violations
8
public class OrderReport {
public String getOrdersInfo(Date startDate, Date endDate) { …}
protected List<Order> queryDBForOrders(Date startDate, Date endDate) { … }
protected String format( Orders orders) { … }
}
Single Responsibility Principle (Solution) 1/3
Creating an Order repository
If you’re not using JPA - Java Persistence API, like in Spring Boot, where you only define the interface, and JPA/Hibernate produces the implementation at runtime and JPA’s repository query method naming syntax.
�This is how you could do the filtering yourself via Java streams:
9
public class OrderRepository extends JpaRepository<Order, Long> {
�public List<Order> findByOrderDateBetween(Date startDate, Date endDate);
}
public List<Order> findByOrderDateBetween(Date startDate, Date endDate) {
return orders.stream()
.filter(order -> order.getOrderDate().isAfter(startDate) || order.getOrderDate().isEqual(startDate))
.filter(order -> order.getOrderDate().isBefore(endDate) || order.getOrderDate().isEqual(endDate))
.collect(Collectors.toList());
}
Single Responsibility Principle (Solution) 2/3
public class OrderHTMLFormatter implements OutputFormatter<Order> {
private static final String HTML_ORDER =
"<div class=\"order-item\" id=\"order-%d\"><div>Order Date</div><div>%s</div>
<div>Ordered By</div><div>%s</div>…</div>";
@Override
public String output(List<Order> orders) {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
return orders.stream()
.map(order -> String.format(HTML_ORDER, order.getId(), dateFormat.format( order.getOrderDate() ), order.getOrderedBy()))
.collect(Collectors.joining("\n")); // Join results with a newline
}
}
public interface OutputFormatter<T> {
String output(List<T> elements);
}
10
Single Responsibility Principle (Solution) 3/3
So now, we’ve created a repository and an output formatter. Here is a refactored order report class, using the new .
public class OrderReport {
� protected OrderRepository repository;� protected OutputFormatter<Order> formatter;
�public OrderReport(OrderRepository repository, OutputFormatter<Order> formatter) {
this.repository = repository;�this.formatter = formatter;
}
�public String getOrdersInfo(Date startDate, Date endDate) {
List<Order> orders = repository.findByOrderDateBetween(startDate, endDate);�return formatter.output(orders);
}
}
11
Open-Closed Principle
Classes should be open for extension but closed for modification.
13
Open-Closed Principle (Definition)
Classes should be extendable, via sub classes, not by changing the contents of the superclass you want to extend.
Goal
14
Open-Closed Principle (Violation)
15
public class CostManager {
public double costPerUnit = 1.5;
�public double calculate(Shape shape) {
double area = 0;
if (shape instanceof Rectangle) {
area = shape.width * spape.height;
} else if(shape instanceof Circle){
area = shape.radius * shape.radius * Math.PI;
} else { throw new ShapeNotFoundException(…);}
�return costPerUnit * area;
}
}
Violations
public class Shape {…}
public class Rectangle extends Shape {
public int width, height;
�public Rectangle(int width, int height) { …… }
}
�public class Circle extends Shape {
public double radius;
�public Circle(double radius) { … }
}
Open-Closed Principle (Solution)
16
public class CostManager {
public double costPerUnit = 1.5;
�public double calculate(IShape shape) {
return costPerUnit * shape.getArea();
}
}
public interface IShape {
public double getArea();
}
public class Rectangle implements IShape {
public int width, height;
public Rectangle(int width, int height) {...}
�public double getArea() { return height * width;}
}
public class Circle implements IShape {
public double radius;�public Circle(double radius) {...}
public double getArea() { return radius * radius * Math.PI;}
}
Liskov Substitution Principle
A subclass should be able to imitate its superclass.
18
Liskov Substitution Principle (Definition)
Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.
Goal
19
Liskov Substitution Principle (Violation)
20
public interface IData {
public Object getAll();
}
Violations
public class FileData implements IData {
�public Object getAll() {
return new List();
}
}
public class DbData implements IData {
�public Object getAll() {
return new MyOwnType();
}
}
Liskov Substitution Principle (Solution)
21
public interface IData {
public List getAll();
}
public class FileData implements IData {
�public List getAll() {
return new List();
}
}
public class DbData implements IData {
�public List getAll() {
return new List<MyOwnType()>;
}
}
Interface Segregation Principle
A client should not be forced to implement an interface that it doesn’t use.
23
Interface Segregation Principle (Definition)
To force a class to implement useless actions, is wasteful and may produce unexpected bugs if the class does not have the ability to perform those actions.
Goal
24
Interface Segregation Principle (Violation)
25
Violations
public interface IWorker {
public void work();
public void sleep();
}
�
public class HumanWorker implements IWorker {
public void work() { Logger.log(‘Working…’); }
public void sleep() { Logger.log(‘Sleeping…’); }
}
public class RobotWorker implements IWorker {
public void work() { Logger.log(‘Working…’); }
�public void sleep() {
// Not needed for robots, they don’t sleep.
Logger.log(‘I am still working! I do not sleep!...’);
}
}
Interface Segregation Principle (Solution)
26
public interface IWorkable {
public void work();
}
public class HumanWorker implements IWorkable, ISleepable {
public void work() { Logger.log(‘Working…’); }
public void sleep() { Logger.log(‘Sleeping…’); }
}
public interface ISleepable {
public void sleep();
}
public class RobotWorker implements IWorkable {
public void work() { Logger.log(‘Working…’); }
}
Interface segregation, segregated a bigger and broader interface into single method interfaces. So now the consumer can get exactly the features it needs. No less no more.
Dependency Inversion Principle
High-level classes should not depend on low-level classes. Both should depend on abstractions.
28
Dependency Inversion Principle (Definition)
A class should not be fused with the tool it uses to execute an action. It should be fused to an interface that will allow the tool to connect to the class.
Goal
29
Dependency Inversion Principle (Violation)
30
Violations
public class DataConsumer {
private MySQLConnection connection;
�public DataConsumer(MySQLConnection connection) {
this.connection = connection;
}
}
public class MySQLConnection {
�public Connection connect() {
System.out.println(‘Connecting…’);
…
}
}
Dependency Inversion Principle (Solution)
31
public class DataConsumer {
private IDataConnection connection;
�public DataConsumer(IDataConnection connection) {
this.connection = connection;
}
}
public interface IDataConnection {
public Connection connect(String url, String userId, String password);
…
}
public class PostgreSQLConnection implements IDataConnection {
public Connection connect(String url, String userId, String password) {
Logger.log(‘Connecting to messaging broker…’);
// Ex: url="jdbc:postgresql://host:port/db?ssl=true"
this.connection = DriverManager.getConnection(url, userId, password);
return this.connection;
}
}
public class MySQLConnection implements IDataConnection {
public Connection connect(String url, String userId, String password) {
Logger.log(‘Connecting to MySQL database…’);
// Ex: url="jdbc:mysql://host:port/db?ssl=true"
this.connection = DriverManager.getConnection(url, userId, password);
return this.connection;
}
}
Now the DataConsumer class depends on an abstraction (interface), enabling it to change connection type dynamically at run-time. Like switching from MySQL to PostgreSQL or any other JDBC-based implementation.
It is more testable and easier to unit-test, when mocking the connection interface (with no logic/behavior) vs. a class implementation (with logic/behavior) having behaviors that generate side effects, which the test has to adjust to.
Questions?
Anything? What’s on your mind? Come on ask me anything…
Feedback?
Thank you for your precious time.
I hope it was worth it and would love to get your feedback.