
What Are SOLID Principles? Explained with Examples

SOLID principles are five fundamental design principles that help developers write more understandable, flexible, and maintainable code. These principles are widely used in object-oriented programming and help reduce code duplication while making applications easier to develop. So, what are the SOLID principles, and how can they be applied?
What Are the SOLID Principles?
SOLID is an acronym formed from the initials of five design principles used in object-oriented programming. These principles aim to organize the responsibilities of software components and make dependencies between them more manageable.
The SOLID approach becomes particularly important as software projects grow. Being able to add new features without having to modify multiple parts of the existing code makes the software development process easier.
The SOLID principles are:
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
These principles are an important part of the software design approach popularized by Robert C. Martin. Each focuses on a different problem, but when applied together, they help create a more sustainable codebase.
1. What Is the Single Responsibility Principle?
The Single Responsibility Principle states that a class should have only one primary reason to change. In other words, a class should not take on multiple unrelated responsibilities at the same time.
For example, in an e-commerce application, placing order creation, payment processing, and email delivery operations in a single class can make the code increasingly complex over time.
Separating these responsibilities into different classes provides a more organized structure:
class OrderService:
def create_order(self, order):
print("Sipariş oluşturuldu.")
class PaymentService:
def process_payment(self, amount):
print("Ödeme gerçekleştirildi.")
class EmailService:
def send_confirmation(self, email):
print("Onay e-postası gönderildi.")In this example, each class fulfills a different responsibility. When the payment process changes, there is no need to modify the order creation or email delivery code.
When should you use it? Consider separating responsibilities when you notice that a class performs too many different tasks, changes frequently, or becomes difficult to test.
2. What Is the Open/Closed Principle?
The Open/Closed Principle states that software components should be open for extension but closed for modification whenever possible.
Imagine an application that supports different payment methods. Adding new conditions to an existing payment class every time a new payment method is introduced can make the code increasingly complex.
Instead, a common interface can be defined:
class PaymentMethod:
def pay(self, amount):
raise NotImplementedError
class CreditCardPayment(PaymentMethod):
def pay(self, amount):
print("Kredi kartı ile ödeme yapıldı.")
class BankTransferPayment(PaymentMethod):
def pay(self, amount):
print("Banka transferi ile ödeme yapıldı.")When a new payment method is needed, a new class that implements the PaymentMethod structure can be created instead of modifying existing classes.
This approach makes it easier to add new features while reducing the risk of breaking existing behavior. However, not every change requires a new class. The goal is to prevent unnecessary modifications in areas that genuinely need to support future extensions.
3. What Is the Liskov Substitution Principle?
The Liskov Substitution Principle states that subclasses should be able to replace their parent classes without disrupting the expected behavior of the program. When a subclass inherits the features of a parent class, it should also preserve the meaning and expected behavior of those features.
For example, designing a Bird class that assumes all birds can fly can create problems because some birds, such as penguins, cannot fly.
class Bird:
def move(self):
print("Kuş hareket ediyor.")
class Sparrow(Bird):
def move(self):
print("Serçe uçarak hareket ediyor.")
class Penguin(Bird):
def move(self):
print("Penguen yürüyerek hareket ediyor.")In this example, the move() method provides general behavior that allows all birds to move. Subclasses implement this behavior according to their own characteristics.
If the parent class had a method such as fly() that required every bird to fly, the Penguin class would not be able to comply with that design. In such cases, redesigning the class hierarchy or defining the flying behavior in a separate interface may be a better solution.
When should you use it? You can apply this principle when using inheritance to verify that subclasses meet the expectations established by their parent classes.
4. What Is the Interface Segregation Principle?
The Interface Segregation Principle states that a class should not be forced to implement methods it does not use. To achieve this, smaller interfaces designed for specific purposes should be preferred over large, comprehensive interfaces.
For example, imagine a software system with a single device interface that includes printing, scanning, and faxing operations. A device that can only print may be forced to implement scanning and faxing methods that it does not support.
Instead, these capabilities can be separated into individual interfaces:
class Printable:
def print_document(self):
raise NotImplementedError
class Scannable:
def scan_document(self):
raise NotImplementedError
class SimplePrinter(Printable):
def print_document(self):
print("Belge yazdırılıyor.")
class MultiFunctionPrinter(Printable, Scannable):
def print_document(self):
print("Belge yazdırılıyor.")
def scan_document(self):
print("Belge taranıyor.")In this structure, SimplePrinter implements only the printing behavior it supports. MultiFunctionPrinter, on the other hand, can use both printing and scanning capabilities.
As a result, classes do not depend on behaviors they do not need. This principle helps make code easier to understand, especially in systems with many different capabilities.
5. What Is the Dependency Inversion Principle?
The Dependency Inversion Principle states that high-level business logic should not depend directly on specific low-level implementations. Instead, both should depend on abstractions, which define behavior without tying the code to a particular implementation.
For example, if a notification system depends exclusively on a specific email delivery class, adding SMS notifications in the future may become difficult. Defining notification behavior through a common interface makes it easier to use different implementations.
class Notification:
def send(self, message):
raise NotImplementedError
class EmailNotification(Notification):
def send(self, message):
print(f"E-posta gönderildi: {message}")
class SMSNotification(Notification):
def send(self, message):
print(f"SMS gönderildi: {message}")
class OrderService:
def __init__(self, notification):
self.notification = notification
def complete_order(self):
self.notification.send("Siparişiniz tamamlandı.")OrderService does not need to know whether the notification is sent by email or SMS. It simply uses the send() method of the notification object provided to it.
This means the notification type can be changed without rewriting the order service's business logic. It also becomes easier to use mock objects in tests instead of classes that send real notifications.
When should you use it? Consider introducing abstractions when a component is tightly coupled to a specific database, notification service, or external service.
Why Are the SOLID Principles Important?
The SOLID principles help keep code understandable and manageable not only when it is first developed but also as changes are introduced over time. Clear responsibilities make collaboration easier, particularly when multiple developers work on the same project.
The main benefits of applying these principles include:
- Easier maintenance: Finding and updating the relevant section of code becomes easier when changes are needed.
- Improved testability: Unit tests can be easier to write when components are separated from one another.
- Extensibility: New features can be added without modifying the entire existing codebase.
- Code reusability: Classes designed to fulfill specific responsibilities can be reused in different parts of an application.
- Better team collaboration: It becomes easier to understand what each part of the code does and why.
However, applying the SOLID principles does not mean creating as many classes or interfaces as possible in every project. Excessive abstraction can make code harder to read and develop, especially in smaller projects. Design decisions should therefore be based on the actual needs of the application.
When learning the SOLID principles, it is useful to understand each one individually and apply it through small examples. Over time, seeing how these principles work together makes it easier to make sound design decisions in larger software projects.



