Lecture 11 - OO Design Principles (SOLID)
Lecture 11 - OO Design Principles (SOLID)
International University
School of Computer Science and Engineering
(IT069IU)
🌐 leduytanit.com 1
Previously,
- Generic
- Generic Class
- Bounded type parameters
- Generic Method
2
Agenda
- Object Oriented Design Principles: SOLID
- S: Single responsibility
- O: Open/closed principle
- L: Liskov substitution principle
- I: Interface segregation principle
- D: Dependency inversion principle
3
So far,
We have learnt about the four main OOP principles:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
4
Object Oriented Design
(OOD) Principles
Golden rules used by object-oriented developers since the early 2000s!
5
Intro
SOLID is an acronym for the first five object-oriented design (OOD) principles by
Robert Martin (also known as Uncle Bob). They are a set of rules and best practices to
follow while designing a class structure.
6
S - Single-Responsibility Principle (SRP)
Single-responsibility Principle (SRP) states:
“A class should have only one reason to change, meaning that a class should have only
one job.”
Explanation:
7
S - Single-Responsibility Principle (SRP)
8
SRP Examples 1
- Imagine that employees of a software company need to do 1 of 3 things:
- software programmer (developer),
- software tester (tester),
- software salesman (salesman).
- Each employee will have a title and based on the title will do the corresponding job.
- Question: Then should you design class “Employee” with property “position” and 3
methods developSoftware(), testSoftware() and saleSoftware()?
class Employee
{
string position;
void developSoftware(){};
void testSoftware(){};
void saleSoftware(){};
} 9
SRP Examples 2
- The answer is NO.
- Imagine if there was one more position, human resource manager, we
would have to modify the "Employee" class, add a new method?
- What if there were 10 more positions?
- At that time, the created objects will have a lot of redundant methods:
Developer does not need to use testSoftware() and saleSoftware() functions
right, accidentally using the wrong method will also have unpredictable
consequences.
- Solution with the principle of Single Responsibility: 1 responsibility per
class. We will create an abstract class "Employee" whose method is
working(), from here you inherit 3 classes namely Developer, Tester and
Salesman.
- In each of these classes, you will implement a specific working()
method, depending on the task of each person.
10
SRP Examples 2
The program above does not follow SRP because RegisterUser does three different
jobs: register a user, connect to the database, and send an email.
Problem: This type of class would cause confusion in larger projects, as it is unexpected to
have email generation in the same class as the registration.
There are also many things that could cause this code to change like if we make a switch
11
in a database schema or if we adopt a new email API to send emails.
SRP Solution 2
Instead, we need to split the class into three specific classes that each accomplish a single job.
Here’s what our same class would look like with all other jobs refactored to separate classes:
This achieves the SRP because RegisterUser only registers a user and the only reason it would
change is if more username restrictions are added. All other behavior is maintained in the
program but is now achieved with calls to userDatabase and emailService.
12
O - Open-Closed Principle (OCP)
Open-Closed Principle (OCP) states:
Explanation:
14
OCP Example 1
We need a class to handle the connection to the database. The original design
only included SQL Server and MySQL. The original design looks like this:
class ConnectionManager
{
public void doConnection(Object $connection)
{
if($connection instanceof SqlServer) {
//connect with SqlServer
} elseif($connection instanceof MySql) {
//connect with MySql
}
}
}
Problem: Unfortunately we, as the lazy developer for the book store, did not design the
classes to be easily extendable in the future. So in order to add this feature, we have
modified the BookStore class. If our class design obeyed the Open-Closed principle
17
we would not need to change this class.
OCP Solution 2
So, as the lazy but clever developer for the book store, we see the design problem
and decide to refactor the code to obey the principle.
interface BookStore {
We change the type of BookStore to Interface and add a save method. Each class
will implement this save method.
public class DatabaseStore implements BookStore { public class FileStore implements BookStore {
@Override @Override
public void save(Book book) { public void save(Book book) {
// Save to DB // Save to file
} }
} }
18
OCP Solution 2
So our class structure now looks like this:
Now our persistence logic is easily extendable. If our boss asks us to add another database and
have 2 different types of databases like MySQL and MongoDB, we can easily do that.
You may think that we could just create multiple classes without an interface and add a save
method to all of them.
We can now pass any class that implements the BookStore interface to this class with the help of
polymorphism. This is the flexibility that interfaces provide. 19
OCP Example 3
The shipping logic of an order is placed inside the Order class.
Assuming the system needs to add a new shipping method, we have to add another
case in the calculateShipping method. This will make the code very difficult to manage.
20
OCP Solution 3
Instead, we should decouple the shipping handling logic into a Shipping interface.
Interface Shipping will have many implementations for each form of transportation:
GroundShipping, AirShipping, ...
public interface Shipping {
long calculate();
}
@Override
public long calculate() {
// Calculate for ground shipping
}
}
@Override
public long calculate() {
// Calculate for air shipping
}
}
23
LSP Example
We have the Animal interface and two implementations of Bird and Dog as follows:
public interface Animal {
void fly();
}
@Override
public void fly() {
// Flying...
}
}
@Override
public void fly() {
// Dog can't fly
throw new UnsupportedOperationException();
}
}
It is clear that the Dog class violates the principle of Liskov substitution.
24
LSP Solution
The solution here would be: create a FlyableAnimal interface as follows:
void fly();
}
@Override
public void fly() {
// Flying...
}
}
25
I: Interface Segregation Principle (ISP)
Interface Segregation Principle (ISP) states:
“Many client-specific interfaces are better than one general-
purpose interface. A client should never be forced to
implement an interface that it doesn’t use, or clients shouldn’t
be forced to depend on methods they do not use.”
Explanation:
- Segregation means keeping things separated, and the Interface Segregation Principle is
about separating the interfaces.
- Larger interfaces should be split into smaller ones. By doing so, we can ensure that
implementing classes only need to be concerned about the methods that are of interest to
them.
- This principle is easy to understand. Imagine we have a large interface, about 100 methods.
The implementation will be very difficult because these interface classes will be forced to
implement all the methods of the interface. There can also be redundancy because a class
does not need to use all 100 methods. When separating the interface into many small
26
interfaces, including related methods, the implementation and management will be easier.
I: Interface Segregation Principle (ISP)
27
ISP Example 1
We have an Animal interface as follows:
interface Animal {
void eat();
void run();
void fly();
We have two classes Dog and Snake that implement the Animal interface. But it's
silly, how can Dog fly(), just like Snake can't run()?
28
ISP Solution 1
- Instead, we should split into 3 interfaces like this:
interface Animal {
void eat();
void run();
void fly();
} 29
ISP Example 2
We modeled a very simplified parking lot. It is the type of parking lot where you
pay an hourly fee.
class Car {
// Some implementation here…
30
ISP Problem 2
Now consider that we want to implement a parking lot that is free.
public class FreeParking implements ParkingLot {
Our parking lot interface was composed
@Override
public void parkCar() { of 2 things: parking related logic (park
} car, unpark car, get capacity) and
@Override payment related logic.
public void unparkCar() {
@Override
public void doPayment(Car car) {
throw new Exception("Parking
lot is free");
}
31
}
ISP Solution 2
We've now separated the parking lot. With this new model, we can even go further
and split the PaidParkingLot to support different types of payment.
Now our model is much more flexible, extendable, and the clients do not need to
implement any irrelevant logic because we provide only parking-related
functionality in the parking lot interface.
32
D: Dependency Inversion Principle (DIP)
Dependency Inversion Principle (DIP) states:
“Entities must depend on abstractions, not on concretions. It states that the high-level module
must not depend on the low-level module, but they should depend on abstractions.”
Explanation:
- Our classes should depend upon interfaces or abstract classes instead of concrete classes
and functions.
- This principle is related to Open-Closed Principles (OCP).
33
D: Dependency Inversion Principle (DIP)
34
DIP Example 1
To demonstrate this, let's bring to life a Windows computer with code:
But what good is a computer without a monitor and keyboard? Let's add one of each to
our constructor so that every computer comes with a Monitor and a
StandardKeyboard:
public class WindowsComputer { This code will work, and we'll be able to use the
StandardKeyboard and Monitor freely within our
private final StandardKeyboard keyboard; WindowsComputer class.
private final Monitor monitor;
public WindowsComputer() {
Problem solved? Not quite. By declaring the
monitor = new Monitor(); StandardKeyboard and Monitor with the new keyword,
keyboard = new StandardKeyboard(); we've tightly coupled these three classes together.
}
Not only does this make our WindowsComputer hard to
} test, but we've also lost the ability to switch out our
StandardKeyboard class with a different new one. And
we're stuck with our Monitor class too. 35
DIP Solution 1
Let's decouple our machine from the StandardKeyboard by adding a more general Keyboard
interface and using this in our class:
Here, we're using the dependency injection pattern to facilitate adding the Keyboard dependency into
the WindowsComputer class. Let's also modify our StandardKeyboard class to implement the Keyboard
interface so that it's suitable for injecting into the WindowsComputer class:
Now our classes are decoupled and communicate through the Keyboard abstraction. If we want, we 36
can easily switch out the type of keyboard in our machine with a different implementation of the
DIP Example 2
For example, we have 2 low-level class BackendDeveloper and FrontendDeveloper and 1 high-
level class Project uses the above 2 classes:
public class BackendDeveloper {
Suppose if later, the project changes technology. Backend developers don't code Java anymore but switch to C
code. Frontend developers don't code pure JS anymore but upgrade to JS frameworks like AngularJS. Obviously
we have to fix not only the code in the low-level classes (BackendDeveloper and FrontendDeveloper) but also the
code in the high-level classes (Project) that are using those low-level classes. This shows that high-level classes
are having to depend on low-level classes. 37
DIP Solution 2
At this point, we will add a Developer abstraction to which the above modules depend:
public interface Developer {
void develop();
}
@Override
public void develop() {
codeJS();
// codeAngular();
}
39
Recap
- Object Oriented design principles: SOLID
- S: Single responsibility
- O: Open/closed principle
- L: Lisko substitution principle
- I: Interface segregation principle
- D: Dependency inversion principle
40
Other Popular Principles
- DRY: “Don’t Repeat Yourself”
41
Thank you for your listening!
42