Sobes.tech
Middle+

What are the SOLID principles and how are they applied in software development?

sobes.tech AI

Answer from AI

SOLID is a set of five principles of object-oriented design aimed at creating more flexible, maintainable, and scalable code.

  • Single Responsibility Principle (SRP): A class should have only one reason to change.
  • Open/Closed Principle (OCP): Software entities (classes, modules, functions) should be open for extension but closed for modification.
  • Liskov Substitution Principle (LSP): Subtypes should be replaceable for their base types without affecting the correctness of the program.
  • Interface Segregation Principle (ISP): Clients should not depend on interfaces they do not use.
  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Application in development:

  • SRP: Dividing functionality into small, specialized classes (e.g., a separate class for network operations, another for database operations).

  • OCP: Using protocols (interfaces) and abstract classes to extend behavior without changing existing code (e.g., implementing strategies).

    // Example of OCP with protocol
    protocol PaymentStrategy {
        func processPayment(amount: Double)
    }
    
    class CreditCardPayment: PaymentStrategy {
        func processPayment(amount: Double) {
            // Credit card payment logic
        }
    }
    
    class PaypalPayment: PaymentStrategy {
        func processPayment(amount: Double) {
            // PayPal payment logic
        }
    }
    
    class PaymentProcessor {
        let strategy: PaymentStrategy
    
        init(strategy: PaymentStrategy) {
            self.strategy = strategy
        }
    
        func makePayment(amount: Double) {
            strategy.processPayment(amount: amount)
        }
    }
    
  • LSP: Guarantee that inheriting classes do not violate the expected behavior of the base class (e.g., when working with collections).

    // Example violating LSP
    class Bird {
        func fly() { print("Flying") }
    }
    
    class Ostrich: Bird {
        // Overriding fly, which does not make sense for an ostrich
        // Violates user expectations that Bird can fly.
        override func fly() { fatalError("Ostrich cannot fly") }
    }
    
    let birds: [Bird] = [Bird(), Bird(), Ostrich()]
    // During iteration, calling fly() on Ostrich may cause a crash
    for bird in birds {
        bird.fly()
    }
    
  • ISP: Creating small, specific protocols instead of one large one (e.g., a separate protocol for data loading, another for displaying).

    // Example of ISP
    protocol DataLoading {
        func loadData()
    }
    
    protocol DataDisplaying {
        func displayData()
    }
    
    class ViewController: DataLoading, DataDisplaying {
        func loadData() {
            // Data loading
        }
    
        func displayData() {
            // Data display
        }
    }
    
    class DataLoader: DataLoading {
        func loadData() {
            // Only data loading
        }
    }
    
  • DIP: Using dependency injection to invert dependencies (e.g., passing dependencies through the constructor).

    // Example of DIP
    protocol NetworkService {
        func fetchData()
    }
    
    class APIService: NetworkService {
        func fetchData() {
            // Implementation of fetching data from API
        }
    }
    
    class DataManager {
        let networkService: NetworkService // Dependency on abstraction
    
        init(networkService: NetworkService) {
            self.networkService = networkService
        }
    
        func getData() {
            networkService.fetchData()
        }
    }
    
    // Injecting dependency
    let api = APIService()
    let manager = DataManager(networkService: api)
    manager.getData()