Applying SOLID principles to Swift is valuable because it enhances code quality, maintainability, and scalability while leveraging Swift’s unique features like protocols, value types, and strong type safety. Swift’s modern approach to object-oriented and protocol-oriented programming aligns well with SOLID, making it essential for developers aiming to write modular, testable, and flexible code.
In this post we will revieew 5 principles by examples.
S.O.L.I.D. principles
The SOLID principles are a set of five design guidelines that help developers create more maintainable, flexible, and scalable software. These principles were introduced by Robert C. Martin, also known as Uncle Bob, and are widely adopted in object-oriented programming. The acronym SOLID stands for: Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP). Each principle addresses a specific aspect of software design, such as ensuring that a class has only one reason to change (SRP), allowing systems to be extended without modifying existing code (OCP), ensuring derived classes can substitute their base classes without altering behavior (LSP), creating smaller, more specific interfaces instead of large, general ones (ISP), and depending on abstractions rather than concrete implementations (DIP).
By adhering to the SOLID principles, developers can reduce code complexity, improve readability, and make systems easier to test, debug, and extend. For example, SRP encourages breaking down large classes into smaller, more focused ones, which simplifies maintenance. OCP promotes designing systems that can evolve over time without requiring extensive rewrites. LSP ensures that inheritance hierarchies are robust and predictable, while ISP prevents classes from being burdened with unnecessary dependencies. Finally, DIP fosters loose coupling, making systems more modular and adaptable to change. Together, these principles provide a strong foundation for writing clean, efficient, and sustainable code.
SRP-Single responsability principle
The Single Responsibility Principle (SRP) is one of the SOLID principles of object-oriented design, stating that a class or module should have only one reason to change, meaning it should have only one responsibility or job. This principle emphasizes that each component of a system should focus on a single functionality, making the code easier to understand, maintain, and test. By isolating responsibilities, changes to one part of the system are less likely to affect others, reducing the risk of unintended side effects and improving overall system stability. In essence, SRP promotes modularity and separation of concerns, ensuring that each class or module is cohesive and focused on a specific task.
// Violating SRP
class EmployeeNonSRP {
let name: String
let position: String
let salary: Double
init(name: String, position: String, salary: Double) {
self.name = name
self.position = position
self.salary = salary
}
func calculateTax() -> Double {
// Tax calculation logic
return salary * 0.2
}
func saveToDatabase() {
// Database saving logic
print("Saving employee to database")
}
func generateReport() -> String {
// Report generation logic
return "Employee Report for \(name)"
}
}
// Adhering to SRP
class Employee {
let name: String
let position: String
let salary: Double
init(name: String, position: String, salary: Double) {
self.name = name
self.position = position
self.salary = salary
}
}
class TaxCalculator {
func calculateTax(for employee: Employee) -> Double {
return employee.salary * 0.2
}
}
class EmployeeDatabase {
func save(_ employee: Employee) {
print("Saving employee to database")
}
}
class ReportGenerator {
func generateReport(for employee: Employee) -> String {
return "Employee Report for \(employee.name)"
}
}
In the first example, the Employee
class violates SRP by handling multiple responsibilities: storing employee data, calculating taxes, saving to a database, and generating reports.
The second example adheres to SRP by separating these responsibilities into distinct classes. Each class now has a single reason to change, making the code more modular and easier to maintain.
OCP-Open/Close principle
The Open/Closed Principle (OCP) is one of the SOLID principles of object-oriented design, stating that software entities (such as classes, modules, and functions) should be open for extension but closed for modification. This means that the behavior of a system should be extendable without altering its existing code, allowing for new features or functionality to be added with minimal risk of introducing bugs or breaking existing functionality. To achieve this, developers often rely on abstractions (e.g., interfaces or abstract classes) and mechanisms like inheritance or polymorphism, enabling them to add new implementations or behaviors without changing the core logic of the system. By adhering to OCP, systems become more flexible, maintainable, and scalable over time.
protocol Shape {
func area() -> Double
}
struct Circle: Shape {
let radius: Double
func area() -> Double {
return 3.14 * radius * radius
}
}
struct Square: Shape {
let side: Double
func area() -> Double {
return side * side
}
}
// Adding a new shape without modifying existing code
struct Triangle: Shape {
let base: Double
let height: Double
func area() -> Double {
return 0.5 * base * height
}
}
// Usage
let shapes: [Shape] = [
Circle(radius: 5),
Square(side: 4),
Triangle(base: 6, height: 3)
]
for shape in shapes {
print("Area: \(shape.area())")
}
In this example the Shape
protocol defines a contract for all shapes. New shapes like Circle
, Square
, and Triangle
can be added by conforming to the protocol without modifying existing code. This adheres to OCP by ensuring the system is open for extension (new shapes) but closed for modification (existing code remains unchanged).
LSP-Liksov substitution principle
The Liskov Substitution Principle (LSP) is one of the SOLID principles of object-oriented design, named after Barbara Liskov. It states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, a subclass should adhere to the contract established by its superclass, ensuring that it can be used interchangeably without causing unexpected behavior or violating the assumptions of the superclass. This principle emphasizes the importance of designing inheritance hierarchies carefully, ensuring that derived classes extend the base class’s functionality without altering its core behavior. Violations of LSP can lead to fragile code, bugs, and difficulties in maintaining and extending the system.
protocol Vehicle {
func move()
}
class Car: Vehicle {
func move() {
print("Car is moving")
}
func honk() {
print("Honk honk!")
}
}
class Bicycle: Vehicle {
func move() {
print("Bicycle is moving")
}
func ringBell() {
print("Ring ring!")
}
}
func startJourney(vehicle: Vehicle) {
vehicle.move()
}
let car = Car()
let bicycle = Bicycle()
startJourney(vehicle: car) // Output: Car is moving
startJourney(vehicle: bicycle) // Output: Bicycle is moving
In this example, both Car
and Bicycle
conform to the Vehicle
protocol, allowing them to be used interchangeably in the startJourney
function without affecting the program’s behavior
ISP-Interface segregation principle
The Interface Segregation Principle (ISP) is one of the SOLID principles of object-oriented design, which states that no client should be forced to depend on methods it does not use. In other words, interfaces should be small, specific, and tailored to the needs of the classes that implement them, rather than being large and monolithic. By breaking down large interfaces into smaller, more focused ones, ISP ensures that classes only need to be aware of and implement the methods relevant to their functionality. This reduces unnecessary dependencies, minimizes the impact of changes, and promotes more modular and maintainable code. For example, instead of having a single interface with methods for printing, scanning, and faxing, ISP would suggest separate interfaces for each responsibility, allowing a printer class to implement only the printing-related methods.
// Violating ISP
protocol Worker {
func work()
func eat()
}
class Human: Worker {
func work() {
print("Human is working")
}
func eat() {
print("Human is eating")
}
}
class Robot: Worker {
func work() {
print("Robot is working")
}
func eat() {
// Robots don't eat, but forced to implement this method
fatalError("Robots don't eat!")
}
}
// Following ISP
protocol Workable {
func work()
}
protocol Eatable {
func eat()
}
class Human: Workable, Eatable {
func work() {
print("Human is working")
}
func eat() {
print("Human is eating")
}
}
class Robot: Workable {
func work() {
print("Robot is working")
}
}
In this example, we first violate ISP by having a single Worker
protocol that forces Robot
to implement an unnecessary eat()
method. Then, we follow ISP by splitting the protocol into Workable
and Eatable
, allowing Robot
to only implement the relevant work()
method.
DIP-Dependency injection principle
The Dependency Inversion Principle (DIP) is one of the SOLID principles of object-oriented design, which states that high-level modules (e.g., business logic) should not depend on low-level modules (e.g., database access or external services), but both should depend on abstractions (e.g., interfaces or abstract classes). Additionally, abstractions should not depend on details; rather, details should depend on abstractions. This principle promotes loose coupling by ensuring that systems rely on well-defined contracts (interfaces) rather than concrete implementations, making the code more modular, flexible, and easier to test or modify. For example, instead of a high-level class directly instantiating a low-level database class, it would depend on an interface, allowing the database implementation to be swapped or mocked without affecting the high-level logic.
protocol Engine {
func start()
}
class ElectricEngine: Engine {
func start() {
print("Electric engine starting")
}
}
class Car {
private let engine: Engine
init(engine: Engine) {
self.engine = engine
}
func startCar() {
engine.start()
}
}
let electricEngine = ElectricEngine()
let car = Car(engine: electricEngine)
car.startCar() // Output: Electric engine starting
In this example, the Car
class depends on the Engine
protocol (abstraction) rather than a concrete implementation, adhering to the DIP
Conclusions
We have demonstrated how the SOLID principles align with Swift, so there is no excuse for not applying them. You can find source code used for writing this post in following repository.