SOLID Principles with Swift by building Library Management System
Tags: solidprinciples, oop, softwaredesign, swift
Published: 2024-03-31 08:33:04

This article delves into the application of SOLID principles in Swift development through the creation of a library catalog. Exploring each principle—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—readers will grasp how to design flexible and scalable Swift code. Through practical examples, this piece illuminates the importance and implementation of SOLID principles for crafting robust software solutions in Swift.

**1. Single Responsibility**

The Single Responsibility Principle states that a class should be responsible for performing only one task. When a class takes on multiple responsibilities, it becomes more complex, making maintenance difficult and increasing the likelihood of errors.

In the following example, the LibraryManager class is tightly coupled as it assumes the responsibilities of managing both books and users within a single entity.

```
class LibraryManager {
    func addBook(_ book: Book) {
        // Add book to the library
    }
    
    func removeBook(_ book: Book) {
        // Remove book from the library
    }
    
    func registerUser(_ user: User) {
        // Register user
    }
    
    func deleteUser(_ user: User) {
        // Delete user
    }
}
```

**Issue with Initial Design:** Combining user and book management into one class can result in complexities. Modifying one function could unintentionally affect the other, potentially introducing errors.

**Alternate SR Approach:** Divide the responsibilities by creating separate entities: BookManager to handle book-related tasks exclusively, and UserManager to manage operations related solely to users.

```
class BookManager {
    func addBook(_ book: Book) {
        // Add book implementation
    }
    
    func removeBook(_ book: Book) {
        // Remove book implementation
    }
}

class UserManager {
    func registerUser(_ user: User) {
        // Register user implementation
    }
    
    func deleteUser(_ user: User) {
        // Delete user implementation
    }
}
```
**Advantages of SR Approach:** By separating concerns, this approach enhances clarity regarding responsibilities, facilitating easier comprehension, maintenance, and scalability of the software.

**2. Open-Closed Principle (OCP)**

The Open/Closed Principle states that software components should allow for extension without requiring modification to existing code. This principle enables the addition of new features without altering the current implementation, thereby preserving the stability of the system and minimizing the potential for introducing errors.

In the coupled example of a Library Management System, a single class called LibraryItem combines attributes and functionalities intended for both books and magazines, resulting in a mixture of characteristics specific to each type.

```
class LibraryItem {
    var title: String = ""
    var isbn: String = ""
    var author: String? // Specific to books
    var editor: String? // Specific to magazines
}
```

**Issue with Initial Design:** The class's rigid design makes it difficult to incorporate new item types. For example, adding a DVD would demand modifications to the class, posing a risk to existing functionalities.

**Alternate OCP Approach:** To mitigate this issue, employ an abstract LibraryItem class and create specific subclasses like Book and Magazine. This approach facilitates the introduction of new item types without the need to alter the existing code, ensuring flexibility and reducing the likelihood of unintended issues.

```
class LibraryItem {
    var title: String = ""
    var isbn: String = ""
}

class Book: LibraryItem {
    var author: String = ""
}

class Magazine: LibraryItem {
    var editor: String = ""
}
```

**Advantages of OCP Approach:**  This hierarchical structure facilitates seamless incorporation of new item types, safeguarding the fundamental LibraryItem class from disruption.

**3. Liskov Substitution Principle (LSP)**

Subtypes should seamlessly substitute their base types, extending the functionality of the base class while preserving its expected behavior. This principle guarantees that any derived class can be used interchangeably with its base class without causing unexpected outcomes.

In the coupled example of a Library Management System, the DigitalBook class, despite being a subclass, introduces functionalities that are not aligned with those of the parent Book class.

```
class Book {
    func displayDetails() {
        // Display details implementation
    }
}

class DigitalBook: Book {
    func displayDetails() {
        // New behavior implementation
    }
    
    func download() {
        // Download implementation
    }
}
```

**Issue with Initial Approach:** Introducing the download method in the DigitalBook class could lead to unforeseen outcomes when DigitalBook instances are utilized in contexts that anticipate a standard Book.

**Alternate LSP Approach:** Guarantee that DigitalBook expands upon the behavior of Book while adhering to the expected functionalities of the base class.

```
class Book {
    func displayDetails() {
        // Display details implementation
    }
}

class DigitalBook: Book {
    override func displayDetails() {
        // Consistent behavior with parent class
    }
}
```

**Advantages LSP Approach:** This design upholds consistency across the system and simplifies the integration of new subclasses without disrupting existing functionality.

**4.Interface Segregation Principle (ISP)**

 The Interface Segregation Principle suggests that interfaces should be concise, containing only the methods necessary for each implementing class. This prevents classes from becoming overly complex and reduces the chances of implementing unnecessary methods.

In a coupled Library Management System example, an interface named LibraryOperations attempts to combine various library operations, regardless of whether they are applicable to all library items.

```
protocol LibraryOperations {
    func borrow()
    func returnItem()
    func searchCatalog()
    func payFines()
}
```

**Issue with Initial Approach:** The bundled LibraryOperations interface includes methods that may not be applicable to all library items. For example, digital items may not support traditional borrowing, rendering the borrow and returnItem methods irrelevant.

**Alternate ISP Approach:** Address this by dividing the LibraryOperations interface into more specialized interfaces, such as Borrowable, Searchable, and FineManagement. This approach ensures that each interface pertains to specific functionalities, allowing for more flexibility and precise implementation.

```
protocol Borrowable {
    func borrow()
    func returnItem()
}

protocol Searchable {
    func searchCatalog()
}

protocol FineManagement {
    func payFines()
}
```

**Advantages ISP Approach:**
Specialized interfaces enable classes to implement only the methods that align with their behavior, promoting a cleaner and modular system design. This approach enhances code clarity and maintainability by ensuring that each class adheres to its specific responsibilities without unnecessary method implementations.

**5. Dependency Inversion Principle (DIP)**

 The Dependency Inversion Principle dictates that high-level modules should not rely on low-level modules. Instead, both should depend on abstractions, such as interfaces or abstract classes. This principle fosters the creation of flexible systems by promoting decoupling and adaptability to changes.

In a coupled Library Management System example, the NotificationService is tightly coupled with the EmailService, which restricts flexibility when adapting to changes in notification mechanisms.

```
class NotificationService {
    var emailService: EmailService
    
    func sendOverdueNotification(user: User) {
        emailService.sendEmail(to: user.email, message: "Overdue Notification")
    }
}
```

**Issue with Initial Approach:**  The direct reliance on EmailService creates inflexibility. If there's a need to switch notification mechanisms (e.g., to SMS or push notifications), modifications to the NotificationService class become necessary.

**Alternate DIP Approach:** Address this by introducing an abstraction, such as a Notifier protocol, and ensuring that NotificationService depends on this abstraction. This enables seamless substitution of various notification mechanisms without altering the NotificationService class.

```
protocol Notifier {
    func sendNotification(contact: String, message: String)
}

class EmailNotifier: Notifier {
    func sendNotification(contact: String, message: String) {
        // Email notification implementation
    }
}

class SMSNotifier: Notifier {
    func sendNotification(contact: String, message: String) {
        // SMS notification implementation
    }
}

class NotificationService {
    var notifier: Notifier
    
    func sendOverdueNotification(user: User) {
        notifier.sendNotification(contact: user.contact, message: "Overdue Notification")
    }
}
```

**Advantages of DIP Approach:** This design detaches the notification logic from the particular notification method, enhancing the system's adaptability and scalability. Modifying or introducing new notification methods becomes simple without affecting the core notification logic.

**Conclusion:** In summary, the SOLID principles provide essential guidelines for writing clean, maintainable, and scalable code. By following these principles, developers can enhance code quality, facilitate collaboration, and build software systems that are robust and adaptable to change. Embracing SOLID is key to creating resilient and efficient software solutions in today's dynamic technological landscape.

Credit for image: Dev Genius, medium.com