Принципы SOLID — это набор пяти основных принципов, которые помогают разработчикам создавать более понятный, гибкий и поддерживаемый код. Так же знание этих принципов довольно часто спрашивают на собеседованиях. Давайте рассмотрим каждый из этих принципов с примерами нарушения и соблюдения на языке Swift:

Принцип единственной ответственности (Single Responsibility Principle, SRP): Этот принцип гласит, что у каждого класса или структуры должна быть только одна задача. Например, если у вас есть класс для обработки данных из сети, его не следует использовать для отображения данных на экране.

Нарушение принципа SPR:

// NetworkManager только выполняет сетевые запросы
class NetworkManager {
    func fetchData(url: URL) {
        // Запрос к API
    }

    func updateUI() {
        // обновляет пользовательский интерфейс
    }
}

В этом примере класс NetworkManager нарушает принцип SPR, потому что он и получает данные из сети, и обновляет UI.

Соблюдение принципа SPR:

// NetworkManager только выполняет сетевые запросы
class NetworkManager {
    func fetchData(url: URL) {
        // Запрос к API
    }
}

// ViewController управляет отображением данных
class ViewController: UIViewController {
    let networkManager = NetworkManager()

    func updateUI() {
        let url = URL(string: "https://api.example.com")!
        networkManager.fetchData(url: url)
        // Обновить интерфейс пользователя с данными
    }
}

Принцип открытости/закрытости (Open-Closed Principle, OCP): Классы и функции должны быть открыты для расширения, но закрыты для изменений. Это означает, что вы должны быть в состоянии добавить новую функциональность без изменения существующего кода. Это можно сделать с помощью протоколов и расширений в Swift.

Нарушение принципа OCP:

class Animal {
    let name: String

    init(name: String) {
        self.name = name
    }

    func makeSound() {
        if name == "Dog" {
            print("Woof")
        } else if name == "Cat" {
            print("Meow")
        }
    }
}

Здесь класс Animal не закрыт для модификации, потому что если мы захотим добавить новое животное, нам придется изменить метод makeSound.

Соблюдение принципа OCP:

protocol Animal {
    func makeSound()
}

class Dog: Animal {
    func makeSound() {
        print("Woof")
    }
}

class Cat: Animal {
    func makeSound() {
        print("Meow")
    }
}

Теперь каждый класс подписанный под протокол Animal может иметь свою собственную реализацию makeSound, и протокол Animal закрыт для модификаций.

Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP): Это означает, что если у вас есть класс B, который является подклассом класса A, вы должны иметь возможность использовать B везде, где используется A, без изменения поведения программы.

Нарушение принципа LSP:

class Bird {
    func fly() {
        // Реализация полета
    }
}

class Penguin: Bird {
    override func fly() {
        fatalError("Penguins can't fly!")
    }
}

let myBird: Bird = Penguin()
myBird.fly()  // Приведет к ошибке во время выполнения

В этом примере Penguin нарушает LSP, потому что он не может летать, в то время как класс Bird предполагает, что все птицы могут летать.

Соблюдение принципа LSP:

class Bird {
    func move() {
        print("The bird is flying")
    }
}

class Penguin: Bird {
    override func move() {
        print("The penguin is sliding")
    }
}


let myBird: Bird = Penguin()
myBird.move()

Теперь каждый подкласс Bird может иметь свою собственную реализацию move, не нарушая поведение базового класса.

Принцип разделения интерфейса (Interface Segregation Principle, ISP): Это означает, что классы не должны зависеть от методов, которые они не используют. В Swift это можно сделать с помощью протоколов.

Нарушение принципа ISP:

protocol Worker {
    func work()
    func eat()
}

class Robot: Worker {
    func work() {
        // работает
    }

    func eat() {
        fatalError("Robots can't eat")
    }
}

Здесь Robot нарушает ISP, потому что он вынужден реализовать функцию eat, которую он не может использовать.

Соблюдение принципа ISP:

protocol Worker {
    func work()
}

protocol Eater {
    func eat()
}

class Robot: Worker {
    func work() {
        // работает
    }
}

Теперь Robot реализует только функции, которые он действительно может использовать.

Принцип инверсии зависимостей (Dependency Inversion Principle, DIP): Это означает, что классы верхнего уровня не должны зависеть от классов нижнего уровня. Оба они должны зависеть от абстракций.

Нарушение принципа DIP:

class LightBulb {
    func turnOn() {
        // включает свет
    }
}

class Switch {
    let bulb: LightBulb

    init(bulb: LightBulb) {
        self.bulb = bulb
    }

    func toggle() {
        bulb.turnOn()
    }
}

Здесь класс Switch напрямую зависит от конкретного класса LightBulb, что нарушает DIP.

Соблюдение принципа DIP:

protocol Switchable {
    func turnOn()
}

class LightBulb: Switchable {
    func turnOn() {
        // включает свет
    }
}

class Switch {
    let device: Switchable

    init(device: Switchable) {
        self.device = device
    }

    func toggle() {
        device.turnOn()
    }
}

Теперь класс Switch зависит от абстракции, а не от конкретного класса, что соблюдает DIP.

Комментарии (16)


  1. FreeNickname
    06.07.2023 16:46
    +2

    Спасибо, наглядно. Один момент: поправьте, если я не прав, но 'Workable' и 'Eatable' не подойдут в качестве названий протоколов, т.к. Eatable означает, что эту сущность можно съесть, а не что она может есть. Workable, соответственно – что над ней можно работать (к примеру, workable item), ну или, в зависимости от контекста, "осуществимый", к примеру, workable plan, но суть та же – это план, по которому можно работать. План сам не работает.


    1. k1ng Автор
      06.07.2023 16:46

      Спасибо, в случае с 'Eatable' согласен - корректнее будет назвать 'Eater', а вот с 'Workable' не совсем пойму на сколько корректнее будет назвать 'Worker'?


      1. FreeNickname
        06.07.2023 16:46

        Думаю, 'Worker' как раз подойдёт хорошо. Ведь основной атрибут рабочего – сто он выполняет работу. Вдруг мы нвнимаем рептилоидов, а они не едят :) ну или, что вероятнее, требуют свою реализацию eat.

        Как в этой системе назвать человека-рабочего – зависит, на мой взгляд, от контекста. Например, у нас роботы, судя по номенклатуре Robot: Worker, всегда являются рабочими. Нет развлекательных роботов в нашем домене. Если с людьми так же (например, нет людей-директоров, которые не работают на производственной линии (мне представился завод)), то может быть просто Human: Worker. Если могут быть люди, не являющиеся рабочими – надо разбираться :)


  1. perecfx
    06.07.2023 16:46
    +1

    У юных програмистов все еще в хайпе солид? Когда уже выпустят новую "серебряную пулю" для подрастающего поколения?


    1. k1ng Автор
      06.07.2023 16:46

      Всё идёт скорее от работодателей и HR'ов которые любят указывать SOLID в вакансиях и спрашивать на собеседованиях.


  1. kmk
    06.07.2023 16:46

    LSP все еще нарушается: класс наследник не должен менять поведение базового класса.


    1. k1ng Автор
      06.07.2023 16:46
      +2

      Принцип LSP гласит, что объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности этой программы.

      Это означает, что класс-наследник может расширять или специализировать поведение базового класса, но не должен ему противоречить или изменять в такой степени, что нарушается ожидаемое поведение при использовании базового класса.

      В моём примере нет нарушения ожидаемого поведения - оба объекта умеют перемещаться func move(), просто имеют различную реализацию, и могут быть взаимно заменены в вызывающем их коде.


      1. Kenya
        06.07.2023 16:46

        Всё верно. Ибо если наследник не может менять поведение родителя, в чём тогда вообще суть наследования)


  1. vasan
    06.07.2023 16:46

    Какой смысл писать на Swift, когда нет кроссплатформенности?По моему C++ в связке с Objective-C решают эту проблему.


    1. k1ng Автор
      06.07.2023 16:46

      Это из какого года комментарий? И к чему он вообще здесь?


      1. vasan
        06.07.2023 16:46

        И что? Всё равно по быстродействию ваш Swift оказывается в жопе по сравнению с теми же С/C++ ))))


        1. k1ng Автор
          06.07.2023 16:46

          Swift достаточно быстр чтобы закрывать потребность в производительности для 99% задач с которыми сталкиваются iOS и Mac разработчики. И при необходимости из Swift можно вызывать Сишные библиотеки. Зато по скорости разработки Swift сильно опережает и С/С++ и Objective-C, что сейчас зачастую важнее быстродействия кода. И кстати, Swift в несколько раз быстрее Objective-C, который вы зачем-то рекомендовали ранее.


  1. Zetsu
    06.07.2023 16:46

    Всё в очередной раз повторяется... Очень поверхностно, а знаете почему? Потому что прежде чем писать о принципах SOLID нужно почитать книжку дядюшки Боба, и понять о чем он писал. Пожалуйста, просто прочитайте.


    1. k1ng Автор
      06.07.2023 16:46

      Смысл этой статьи – краткость и наглядность. Для более глубокого изучения есть другие статьи и литература.


      1. vasan
        06.07.2023 16:46

        как всё сложно.


  1. Chris_moler
    06.07.2023 16:46

    Кмк srp описан неверно. Цитата из книги «Модуль должен иметь одну и только одну причину для изменени» и это не про то что условный класс должен делать только что то одно. Тут вопрос со стороны акторов этого класса. Если всех устраивает его изменение в поведении - это ок. Если ожидаемое поведение для разных потребителей становится разным - принцип нарушен. Условно ваш networking manager имеет сортировку по убыванию и всех все устраивает, но появляется класс которому нужна сортировка по возрастанию, тут мы видим нарушение.