Solid principles are a set of ve design principles that help software developers write clean,
maintainable, and scalable code. They were introduced by Robert C. Martin (also known as "Uncle
Bob") in the year 2000. Following these principles helps in avoiding common issues like tight
coupling, low readability, and the introduction of bugs. The acronym SOLID stands for:
• S - Single Responsibility Principle
• O - Open/Closed Principle
• L - Liskov Substitution Principle
• I - Interface Segregation Principle
• D - Dependency Inversion Principle
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) states that a class should have only one reason to
change. This means a class should only have a single job or responsibility. A good way to think
about this is that all the methods and properties within a class should be related to a single, well-
de ned purpose.
Problem Without SRP
Let's consider an e-commerce application. A common class you might nd is a ShoppingCart
class. It's easy to fall into the trap of making this one class handle too many tasks. For example, a
single ShoppingCart class could be responsible for:
1. Managing the products in the cart.
2. Calculating the total price.
3. Printing the invoice.
4. Saving the cart data to a database.
This violates the SRP because the ShoppingCart class has multiple reasons to change. If the
logic for calculating the total price changes, we modify this class. If the invoice format changes, we
modify this class. If the database schema for saving the cart changes, we modify this class. This
makes the class dif cult to maintain and more prone to bugs.
fi
fi
fi
fi
Code Example (Violating SRP)
Here is a Java code example of a ShoppingCart class that violates the SRP.
Java
import [Link];
import [Link];
class Product {
private String name;
private double price;
public Product(String name, double price) {
[Link] = name;
[Link] = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
class ShoppingCart {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
[Link](product);
}
public List<Product> getProducts() {
return products;
}
// Responsibility 1: Calculate total price
public double calculateTotal() {
double total = 0;
for (Product product : products) {
total += [Link]();
}
return total;
}
// Responsibility 2: Print invoice
public void printInvoice() {
[Link]("--- Invoice ---");
for (Product product : products) {
[Link]([Link]() + ": $" +
[Link]());
}
[Link]("Total: $" + calculateTotal());
[Link]("----------------");
}
// Responsibility 3: Save to database
public void saveToDatabase() {
[Link]("Saving shopping cart to
database...");
// Imagine complex database logic here
}
}
public class SSRPViolation {
public static void main(String[] args) {
Product laptop = new Product("Laptop", 1500.0);
Product mouse = new Product("Mouse", 50.0);
ShoppingCart cart = new ShoppingCart();
[Link](laptop);
[Link](mouse);
[Link]();
[Link]();
}
}
Output:
--- Invoice ---
Laptop: $1500.0
Mouse: $50.0
Total: $1550.0
----------------
Saving shopping cart to database...
Solution with SRP
To solve this, we should break down the single ShoppingCart class into multiple, more
focused classes, each with a single responsibility.
• The ShoppingCart class will only be responsible for managing products and
calculating the total price.
• We'll create a InvoicePrinter class dedicated to printing invoices.
• We'll create a DatabaseSaver class for saving the cart to a database.
This way, if the invoice format needs to change, we only modify the InvoicePrinter class. If
the database logic changes, we only modify the DatabaseSaver class. The ShoppingCart
class remains untouched. This approach uses the principle of composition, where we create
separate classes and have our main class hold references to them.
Code Example (Following SRP)
Java
import [Link];
import [Link];
class Product {
private String name;
private double price;
public Product(String name, double price) {
[Link] = name;
[Link] = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
// Single Responsibility 1: Manage products and calculate
total
class ShoppingCart {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
[Link](product);
}
public List<Product> getProducts() {
return products;
}
public double calculateTotal() {
double total = 0;
for (Product product : products) {
total += [Link]();
}
return total;
}
}
// Single Responsibility 2: Print invoices
class InvoicePrinter {
public void print(ShoppingCart cart) {
[Link]("--- Invoice ---");
for (Product product : [Link]()) {
[Link]([Link]() + ": $" +
[Link]());
}
[Link]("Total: $" +
[Link]());
[Link]("----------------");
}
}
// Single Responsibility 3: Save to database
class DatabaseSaver {
public void save(ShoppingCart cart) {
[Link]("Saving shopping cart to
database...");
// Complex database logic here
}
}
public class SSRPExample {
public static void main(String[] args) {
Product laptop = new Product("Laptop", 1500.0);
Product mouse = new Product("Mouse", 50.0);
ShoppingCart cart = new ShoppingCart();
[Link](laptop);
[Link](mouse);
InvoicePrinter printer = new InvoicePrinter();
[Link](cart);
DatabaseSaver saver = new DatabaseSaver();
[Link](cart);
}
}
Output:
--- Invoice ---
Laptop: $1500.0
Mouse: $50.0
Total: $1550.0
----------------
Saving shopping cart to database...
2. Open/Closed Principle (OCP)
The Open/Closed Principle (OCP) states that a class should be open for extension but closed for
modi cation. This means you should be able to add new functionality without changing existing
code. OCP is primarily achieved by using abstraction (interfaces or abstract classes) and
polymorphism.
Problem Without OCP
Let's go back to our shopping cart example. Imagine we want to add new ways to save the shopping
cart, such as to a MongoDB database or a le, in addition to the existing SQL database. A common,
but incorrect, approach would be to modify the DatabaseSaver class we created earlier.
We would add new methods like saveToMongo() and saveToFile() to the existing
DatabaseSaver class. This violates OCP because we're modifying an existing class to add new
fi
fi
functionality. Each time we need a new way to save the data, we have to change the
DatabaseSaver class, which increases the risk of introducing bugs to the already-working
methods.
Code Example (Violating OCP)
Here is a Java code example that shows how adding new features by modifying an existing class
violates OCP.
Java
import [Link];
import [Link];
// Assume Product and ShoppingCart classes are the same as in
the SRP example.
class Product {
private String name;
private double price;
public Product(String name, double price) {
[Link] = name;
[Link] = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
class ShoppingCart {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
[Link](product);
}
public List<Product> getProducts() {
return products;
}
public double calculateTotal() {
double total = 0;
for (Product product : products) {
total += [Link]();
}
return total;
}
}
// Violates OCP because we are modifying this class to add
new ways of saving data.
class DatabaseSaver {
public void saveToSQL(ShoppingCart cart) {
[Link]("Saving shopping cart to SQL
database...");
}
// New functionality added by modifying the class
public void saveToMongo(ShoppingCart cart) {
[Link]("Saving shopping cart to MongoDB
database...");
}
// New functionality added by modifying the class
public void saveToFile(ShoppingCart cart) {
[Link]("Saving shopping cart to a
file...");
}
}
public class OCPViolation {
public static void main(String[] args) {
Product laptop = new Product("Laptop", 1500.0);
Product mouse = new Product("Mouse", 50.0);
ShoppingCart cart = new ShoppingCart();
[Link](laptop);
[Link](mouse);
DatabaseSaver saver = new DatabaseSaver();
[Link](cart);
[Link](cart);
[Link](cart);
}
}
Output:
Saving shopping cart to SQL database...
Saving shopping cart to MongoDB database...
Saving shopping cart to a file...
Solution with OCP
To adhere to the OCP, we can introduce an abstraction (an interface or an abstract class) that de nes
the contract for saving data. All speci c saving methods will implement this interface. This allows
us to add new ways to save data without ever touching the existing classes.
1. Create a CartPersistence interface with a save(ShoppingCart cart)
method.
2. Create concrete classes like SQLPersistence, MongoPersistence, and
FilePersistence that implement this interface.
3. Each of these classes will contain the speci c logic for saving to its respective destination.
Now, if a new requirement comes in to save to a Cassandra database, we simply create a new
CassandraPersistence class that implements the CartPersistence interface. We
don't need to modify any of the existing classes.
Code Example (Following OCP)
Java
import [Link];
import [Link];
// Assume Product and ShoppingCart classes are the same.
class Product {
private String name;
private double price;
public Product(String name, double price) {
[Link] = name;
[Link] = price;
fi
fi
fi
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
class ShoppingCart {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
[Link](product);
}
public List<Product> getProducts() {
return products;
}
public double calculateTotal() {
double total = 0;
for (Product product : products) {
total += [Link]();
}
return total;
}
}
// OCP Solution: Abstract base class for persistence
interface CartPersistence {
void save(ShoppingCart cart);
}
// Concrete classes implementing the interface
class SQLPersistence implements CartPersistence {
@Override
public void save(ShoppingCart cart) {
[Link]("Saving shopping cart to SQL
database...");
}
}
class MongoPersistence implements CartPersistence {
@Override
public void save(ShoppingCart cart) {
[Link]("Saving shopping cart to MongoDB
database...");
}
}
class FilePersistence implements CartPersistence {
@Override
public void save(ShoppingCart cart) {
[Link]("Saving shopping cart to a
file...");
}
}
public class OCPExample {
public static void main(String[] args) {
Product laptop = new Product("Laptop", 1500.0);
Product mouse = new Product("Mouse", 50.0);
ShoppingCart cart = new ShoppingCart();
[Link](laptop);
[Link](mouse);
CartPersistence sqlSaver = new SQLPersistence();
[Link](cart);
CartPersistence mongoSaver = new MongoPersistence();
[Link](cart);
CartPersistence fileSaver = new FilePersistence();
[Link](cart);
}
}
Output:
Saving shopping cart to SQL database...
Saving shopping cart to MongoDB database...
Saving shopping cart to a file...
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) states that subclasses should be substitutable for their
base classes. This means that if you have a class A, and a subclass B inherits from A, you should be
able to use an object of type B wherever an object of type A is expected, without the program's
behavior changing unexpectedly.
The video uses the example of a banking system to demonstrate this principle.
Problem Without LSP
Let's imagine a bank has different types of accounts, all inheriting from a base Account class.
• Account (Base Class)
◦ deposit()
◦ withdraw()
• SavingsAccount (Subclass)
◦ Implements deposit() and withdraw()
• CurrentAccount (Subclass)
◦ Implements deposit() and withdraw()
• FixedTermAccount (Subclass)
◦ Implements deposit(), but withdraw() is not allowed. To handle this, the
withdraw() method in FixedTermAccount throws an exception.
The problem arises when a client (e.g., a banking application) tries to use a
FixedTermAccount where an Account is expected. A client expects that any Account
can handle both deposit() and withdraw(). However, if they try to withdraw from a
FixedTermAccount, the program will crash or produce an unexpected error. This violates the
LSP because FixedTermAccount cannot be substituted for its base class Account.
Code Example (Violating LSP)
Java
import [Link];
import [Link];
// Base class
abstract class Account {
public abstract void deposit(double amount);
public abstract void withdraw(double amount);
}
// Subclasses
class SavingsAccount extends Account {
private double balance = 0;
@Override
public void deposit(double amount) {
balance += amount;
[Link]("SavingsAccount: Deposited $" +
amount);
}
@Override
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
[Link]("SavingsAccount: Withdrew $" +
amount);
} else {
[Link]("SavingsAccount: Insufficient
funds.");
}
}
}
class CurrentAccount extends Account {
private double balance = 0;
@Override
public void deposit(double amount) {
balance += amount;
[Link]("CurrentAccount: Deposited $" +
amount);
}
@Override
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
[Link]("CurrentAccount: Withdrew $" +
amount);
} else {
[Link]("CurrentAccount: Insufficient
funds.");
}
}
}
class FixedTermAccount extends Account {
private double balance = 0;
@Override
public void deposit(double amount) {
balance += amount;
[Link]("FixedTermAccount: Deposited $" +
amount);
}
@Override
public void withdraw(double amount) {
// This violates LSP by throwing an exception for a
base class method.
throw new UnsupportedOperationException("Withdrawal
is not allowed in Fixed Term Accounts.");
}
}
// Client class that processes transactions
class BankClient {
public void processTransactions(List<Account> accounts) {
for (Account account : accounts) {
try {
[Link](100);
[Link](50); // This will cause an
exception for FixedTermAccount
} catch (UnsupportedOperationException e) {
[Link]("Error: " +
[Link]());
}
}
}
}
public class LSPViolation {
public static void main(String[] args) {
List<Account> accounts = new ArrayList<>();
[Link](new SavingsAccount());
[Link](new CurrentAccount());
[Link](new FixedTermAccount());
BankClient client = new BankClient();
[Link](accounts);
}
}
Output:
SavingsAccount: Deposited $100.0
SavingsAccount: Withdrew $50.0
CurrentAccount: Deposited $100.0
CurrentAccount: Withdrew $50.0
FixedTermAccount: Deposited $100.0
Error: Withdrawal is not allowed in Fixed Term Accounts.
Solution with LSP
To solve this, we should restructure our class hierarchy to ensure that subclasses only inherit
methods they can fully support. We can break the Account interface into more speci c interfaces.
1. Create a DepositAccount interface with only the deposit() method.
2. Create a WithdrawableAccount interface that extends DepositAccount and
adds a withdraw() method.
3. FixedTermAccount will only implement DepositAccount.
4. SavingsAccount and CurrentAccount will implement
WithdrawableAccount.
Now, the client can work with different types of accounts without expecting unsupported
functionality. For example, a client that only needs to deposit money can work with a list of
DepositAccount, and a client that needs to withdraw can work with a list of
WithdrawableAccount. This ensures that a subclass is always a valid substitute for its
parent.
Code Example (Following LSP)
fi
Java
import [Link];
import [Link];
// Parent interface for accounts that can only deposit
interface DepositAccount {
void deposit(double amount);
}
// Child interface for accounts that can both deposit and
withdraw
interface WithdrawableAccount extends DepositAccount {
void withdraw(double amount);
}
// Subclasses implementing the appropriate interfaces
class SavingsAccount implements WithdrawableAccount {
private double balance = 0;
@Override
public void deposit(double amount) {
balance += amount;
[Link]("SavingsAccount: Deposited $" +
amount);
}
@Override
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
[Link]("SavingsAccount: Withdrew $" +
amount);
} else {
[Link]("SavingsAccount: Insufficient
funds.");
}
}
}
class CurrentAccount implements WithdrawableAccount {
private double balance = 0;
@Override
public void deposit(double amount) {
balance += amount;
[Link]("CurrentAccount: Deposited $" +
amount);
}
@Override
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
[Link]("CurrentAccount: Withdrew $" +
amount);
} else {
[Link]("CurrentAccount: Insufficient
funds.");
}
}
}
class FixedTermAccount implements DepositAccount {
private double balance = 0;
@Override
public void deposit(double amount) {
balance += amount;
[Link]("FixedTermAccount: Deposited $" +
amount);
}
}
// Updated client class with specific transaction processors
class BankClient {
public void processDeposits(List<DepositAccount>
accounts) {
for (DepositAccount account : accounts) {
[Link](100);
}
}
public void processWithdrawals(List<WithdrawableAccount>
accounts) {
for (WithdrawableAccount account : accounts) {
[Link](50);
}
}
}
public class LSPExample {
public static void main(String[] args) {
List<WithdrawableAccount> withdrawableAccounts = new
ArrayList<>();
[Link](new SavingsAccount());
[Link](new CurrentAccount());
List<DepositAccount> depositOnlyAccounts = new
ArrayList<>();
[Link](new FixedTermAccount());
BankClient client = new BankClient();
[Link]("Processing deposits for all
accounts:");
[Link](withdrawableAccounts);
[Link](depositOnlyAccounts);
[Link]("\nProcessing withdrawals for
withdrawable accounts:");
[Link](withdrawableAccounts);
}
}
Output:
Processing deposits for all accounts:
SavingsAccount: Deposited $100.0
CurrentAccount: Deposited $100.0
FixedTermAccount: Deposited $100.0
Processing withdrawals for withdrawable accounts:
SavingsAccount: Withdrew $50.0
CurrentAccount: Withdrew $50.0
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) states that clients should not be forced to depend on
interfaces they do not use. This means it's better to have many small, speci c interfaces than one
large, all-purpose interface.
fi
The video references the FixedTermAccount problem from the LSP section. The problem
there was essentially a violation of ISP: the Account interface was too large and forced the
FixedTermAccount class to implement a withdraw() method it didn't need.
Problem Without ISP
Imagine a large Printer interface with methods for different functionalities:
• print()
• scan()
• fax()
A modern all-in-one printer would implement all of these methods, but what about a simple, old-
school printer that can only print? This printer would be forced to implement the scan() and
fax() methods, even though it doesn't need them. This is a violation of ISP.
Code Example (Violating ISP)
Java
// A bloated, "fat" interface
interface MultiFunctionDevice {
void print();
void scan();
void fax();
}
class AllInOnePrinter implements MultiFunctionDevice {
@Override
public void print() {
[Link]("Printing document...");
}
@Override
public void scan() {
[Link]("Scanning document...");
}
@Override
public void fax() {
[Link]("Faxing document...");
}
}
class SimplePrinter implements MultiFunctionDevice {
@Override
public void print() {
[Link]("Printing document...");
}
@Override
public void scan() {
// This method is not needed, but must be
implemented.
throw new UnsupportedOperationException("Scan not
supported.");
}
@Override
public void fax() {
// This method is not needed, but must be
implemented.
throw new UnsupportedOperationException("Fax not
supported.");
}
}
public class ISPViolation {
public static void main(String[] args) {
MultiFunctionDevice simplePrinter = new
SimplePrinter();
[Link]();
try {
[Link]();
} catch (UnsupportedOperationException e) {
[Link]([Link]());
}
}
}
Output:
Printing document...
Scan not supported.
Solution with ISP
To solve this, we should break the large interface into smaller, more speci c ones. This way, each
class only implements the interfaces it needs.
1. Create a Printer interface with only a print() method.
2. Create a Scanner interface with only a scan() method.
3. Create a Fax interface with only a fax() method.
Now, a simple printer only implements the Printer interface, and a multifunction printer can
implement all three interfaces.
Code Example (Following ISP)
Java
// Small, specific interfaces
interface Printer {
void print();
}
interface Scanner {
void scan();
}
interface Fax {
void fax();
}
class AllInOnePrinter implements Printer, Scanner, Fax {
@Override
public void print() {
[Link]("Printing document...");
}
@Override
public void scan() {
[Link]("Scanning document...");
}
fi
@Override
public void fax() {
[Link]("Faxing document...");
}
}
class SimplePrinter implements Printer {
@Override
public void print() {
[Link]("Printing document...");
}
}
public class ISPExample {
public static void main(String[] args) {
Printer simplePrinter = new SimplePrinter();
[Link]();
Printer allInOnePrinter = new AllInOnePrinter();
[Link]();
Scanner allInOneScanner = (Scanner) allInOnePrinter;
[Link]();
}
}
Output:
Printing document...
Printing document...
Scanning document...
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) states that high-level modules should not depend on
low-level modules; both should depend on abstractions. Additionally, abstractions should not
depend on details; details should depend on abstractions.
This principle is about decoupling your code. High-level modules contain the core business logic of
an application (e.g., a Store class that processes orders), while low-level modules contain the
implementation details (e.g., a Database class that saves data).
Problem Without DIP
If a high-level Store class directly creates an object of a low-level Database class, it creates a
tight coupling. The Store class is directly dependent on the Database class's implementation
details. If the database type changes (e.g., from SQL to MongoDB), the high-level Store class has
to be modi ed, violating the OCP.
Code Example (Violating DIP)
Java
// Low-level module with specific implementation
class MySQLDatabase {
public void save(String data) {
[Link]("Saving " + data + " to a MySQL
database.");
}
}
// High-level module depending directly on the low-level
module
class Store {
private MySQLDatabase database;
public Store() {
[Link] = new MySQLDatabase(); // Tight
coupling
}
public void processOrder(String orderId) {
String data = "Order: " + orderId;
[Link](data);
}
}
public class DIPViolation {
public static void main(String[] args) {
Store store = new Store();
[Link]("ORD123");
}
}
fi
Output:
Saving Order: ORD123 to a MySQL database.
Solution with DIP
To follow DIP, we introduce an abstraction (an interface) between the high-level and low-level
modules.
1. Create a Database interface with a save(String data) method.
2. The high-level Store class will now depend on this Database interface, not a concrete
implementation. The Store class will be given an instance of a class that implements the
Database interface (often through a constructor or method, a process called Dependency
Injection).
3. Low-level classes like MySQLDatabase and MongoDBDatabase will implement the
Database interface.
Now, the high-level module depends on an abstraction, and the low-level modules also depend on
that same abstraction. The high-level module is no longer coupled to the implementation details.
Code Example (Following DIP)
Java
// Abstraction (interface)
interface Database {
void save(String data);
}
// Low-level modules implementing the abstraction
class MySQLDatabase implements Database {
@Override
public void save(String data) {
[Link]("Saving " + data + " to a MySQL
database.");
}
}
class MongoDBDatabase implements Database {
@Override
public void save(String data) {
[Link]("Saving " + data + " to a MongoDB
database.");
}
}
// High-level module depending on the abstraction
class Store {
private Database database; // Depends on the abstraction,
not a concrete class
// Dependency Injection via constructor
public Store(Database database) {
[Link] = database;
}
public void processOrder(String orderId) {
String data = "Order: " + orderId;
[Link](data);
}
}
public class DIPExample {
public static void main(String[] args) {
// High-level module is independent of the low-level
implementation.
// We can easily switch implementations.
Database mySQL = new MySQLDatabase();
Store storeWithMySQL = new Store(mySQL);
[Link]("ORD123");
[Link]();
Database mongoDB = new MongoDBDatabase();
Store storeWithMongoDB = new Store(mongoDB);
[Link]("ORD456");
}
}
Output:
Saving Order: ORD123 to a MySQL database.
Saving Order: ORD456 to a MongoDB database.