Акции и промокоды Отзывы о школах

Пять способов перестать писать некачественный код в iOS

#Блог

В эпоху стремительного развития мобильных технологий качество программного кода становится критически важным фактором успеха. Особенно это касается iOS-разработки, где сложность приложений постоянно растет, а требования к их производительности и масштабируемости становятся все более жесткими. В этом контексте принципы SOLID представляют собой не просто набор теоретических концепций, а практический инструментарий для создания надежного и поддерживаемого кода.

Мы часто сталкиваемся с ситуациями, когда даже опытные разработчики, увлекаясь реализацией функционала, забывают о базовых принципах архитектуры программного обеспечения. Это приводит к появлению сложно поддерживаемого кода, который со временем превращается в настоящий технический долг. В данной статье мы рассмотрим, как применение принципов SOLID может помочь избежать этих проблем в контексте iOS-разработки, и предложим практические примеры их реализации.

Что такое SOLID?

SOLID представляет собой набор из пяти фундаментальных принципов объектно-ориентированного программирования, сформулированных Робертом Мартином. В контексте iOS-разработки эти принципы приобретают особую актуальность, учитывая масштабы и сложность современных мобильных приложений.

Расшифруем этот акроним:

  • Single Responsibility Principle (Принцип единственной ответственности): каждый класс должен иметь только одну причину для изменения
  • Open/Closed Principle (Принцип открытости/закрытости): программные сущности должны быть открыты для расширения, но закрыты для модификации
  • Liskov Substitution Principle (Принцип подстановки Барбары Лисков): объекты базового класса должны быть заменяемы объектами его подклассов без нарушения работы программы
  • Interface Segregation Principle (Принцип разделения интерфейсов): клиенты не должны зависеть от интерфейсов, которые они не используют
  • Dependency Inversion Principle (Принцип инверсии зависимостей): модули верхнего уровня не должны зависеть от модулей нижнего уровня

Lиаграмма, наглядно объясняющая каждый из пяти принципов SOLID

Эти принципы не являются строгими правилами или законами – скорее, это проверенные временем рекомендации, помогающие создавать более качественное программное обеспечение. В мире iOS-разработки, где Swift и его строгая система типов предоставляют мощные инструменты для реализации этих принципов, SOLID становится особенно эффективным подходом к проектированию архитектуры приложений.

Принцип единственной ответственности (Single Responsibility Principle, SRP)

Определение и значение

Принцип единственной ответственности утверждает, что каждый класс должен иметь только одну причину для изменения. В контексте iOS-разработки это означает, что каждый компонент приложения должен отвечать за решение одной конкретной задачи. На практике это помогает избежать создания так называемых «божественных объектов» (god objects), которые пытаются делать слишком много.

Примеры нарушения SRP в iOS

Рассмотрим типичный антипаттерн в iOS-разработке:

class Handler_NOT_SOLID {
func handle() {
    let data = requestDataToAPI()
    guard let dataReceive = data else { return }
    let array = parse(data: dataReceive)
    saveToDB(array: array)
}
   
private func requestDataToAPI() -> Data? {
    // Отправка запроса к API
    return nil
}
   
private func parse(data: Data) -> [String]? {
    // Парсинг данных
    return nil
}
   
private func saveToDB(array: [String]?) {
    // Сохранение в базу данных
}
}

На иллюстрации представлена архитектурная схема класса Handler_NOT_SOLID, которая демонстрирует нарушение принципа единственной ответственности (SRP). Один центральный блок — Handler_NOT_SOLID — соединён стрелками с тремя дочерними блоками: Request, Parse и Save. Каждому из них соответствует пиктограмма: глобус для сетевого запроса, символ кода для парсинга и дискета для сохранения данных.

Здесь класс Handler_NOT_SOLID нарушает SRP, так как отвечает сразу за несколько задач: сетевые запросы, обработку данных и работу с базой данных.

Применение SRP в iOS

Правильная реализация предполагает разделение ответственности:

class APIHandler {
func requestDataToAPI() -> Data? {
    // Отправка запроса к API
    return nil
}
}

class ParseHandler {
func parse(data: Data) -> [String]? {
    // Парсинг данных
    return nil
}
}

class DBHandler {
func saveToDB(array: [String]?) {
    // Сохранение в базу данных
}
}

class Handler_SOLID {
let apiHandler: APIHandler
let parseHandler: ParseHandler
let dbHandler: DBHandler
   
init(apiHandler: APIHandler, parseHandler: ParseHandler, dbHandler: DBHandler) {
    self.apiHandler = apiHandler
    self.parseHandler = parseHandler
    self.dbHandler = dbHandler
}
}

Такая структура обеспечивает лучшую тестируемость, упрощает поддержку кода и делает систему более гибкой к изменениям. Каждый компонент можно модифицировать независимо от других, что особенно важно при работе в команде над крупными iOS-проектами.

Принцип открытости/закрытости (Open/Closed Principle, OCP)

Определение и значение

Принцип открытости/закрытости гласит, что программные сущности должны быть открыты для расширения, но закрыты для модификации. В iOS-разработке это означает возможность добавлять новую функциональность без изменения существующего кода.

Примеры нарушения OCP в iOS

Рассмотрим пример нарушения принципа:

class Vehicles_NOT_SOLID {
func printData() {
    let cars = [
        Car_NOT_SOLID(name: "Tesla", color: "Red"),
        Car_NOT_SOLID(name: "BMW", color: "Black")
    ]
    cars.forEach { print($0.printDetails()) }
 
    let buses = [
        Bus_NOT_SOLID(type: "Electric"),
        Bus_NOT_SOLID(type: "Diesel")
    ]
    buses.forEach { print($0.printDetails()) }
}
}

class Car_NOT_SOLID {
let name: String
let color: String
   
init(name: String, color: String) {
    self.name = name
    self.color = color
}
   
func printDetails() -> String {
    return "name: \(name) color: \(color)"
}
}

class Bus_NOT_SOLID {
let type: String
   
init(type: String) {
    self.type = type
}
   
func printDetails() -> String {
    return "bus type: \(type)"
}
}

При добавлении нового типа транспорта потребуется изменять метод printData().

Применение OCP в iOS

Правильная реализация с использованием протокола:

protocol Printable {
func printDetails() -> String
}

class Vehicles_SOLID {
func printData() {
    let vehicles: [Printable] = [
        Car_SOLID(name: "Tesla", color: "Red"),
        Car_SOLID(name: "BMW", color: "Black"),
        Bus_SOLID(type: "Electric"),
        Bus_SOLID(type: "Diesel")
    ]
    vehicles.forEach { print($0.printDetails()) }
}
}

class Car_SOLID: Printable {
let name: String
let color: String
   
init(name: String, color: String) {
    self.name = name
    self.color = color
}
   
func printDetails() -> String {
    return "name: \(name) color: \(color)"
}
}

class Bus_SOLID: Printable {
let type: String
   
init(type: String) {
    self.type = type
}
   
func printDetails() -> String {
    return "bus type: \(type)"
}
}

Теперь добавление новых типов транспорта не требует изменения существующего кода – достаточно реализовать протокол Printable.

Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP)

Определение и значение

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

Примеры нарушения LSP в iOS

class Rectangle_NOT_SOLID {
var width: Double = 0
var height: Double = 0
var area: Double {
    return width * height
}
}

class Square_NOT_SOLID: Rectangle_NOT_SOLID {
override var width: Double {
    didSet {
        height = width
    }
}
}

func printArea(of rectangle: Rectangle_NOT_SOLID) {
rectangle.width = 10
rectangle.height = 4
print(rectangle.area) // Ожидаем 40, но для Square получим другой результат
}

В этом примере Square нарушает LSP, изменяя поведение базового класса.

Применение LSP в iOS

protocol Shape {
var area: Double { get }
}

class Rectangle_SOLID: Shape {
let width: Double
let height: Double
   
init(width: Double, height: Double) {
    self.width = width
    self.height = height
}
   
var area: Double {
    return width * height
}
}

class Square_SOLID: Shape {
let side: Double
   
init(side: Double) {
    self.side = side
}
   
var area: Double {
    return side * side
}
}

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

Принцип разделения интерфейсов (Interface Segregation Principle, ISP)

Определение и значение

ISP предполагает, что клиенты не должны зависеть от методов, которые они не используют. В iOS-разработке это означает создание специализированных протоколов вместо крупных монолитных интерфейсов.

Примеры нарушения ISP в iOS

protocol GestureProtocol {
func didTap()
func didLongPress()
func didSwipe()
}

class RichButton_NOT_SOLID: GestureProtocol {
func didTap() {
    print("tap button")
}
func didLongPress() {
    print("long press")
}
func didSwipe() {
    print("swipe")
}
}

class PoorButton_NOT_SOLID: GestureProtocol {
func didTap() {
    print("tap")
}
// Пустые реализации для неиспользуемых методов
func didLongPress() {}
func didSwipe() {}
}

Применение ISP в iOS

protocol TapGesture {
func didTap()
}

protocol LongPressGesture {
func didLongPress()
}

protocol SwipeGesture {
func didSwipe()
}

class RichButton_SOLID: TapGesture, LongPressGesture, SwipeGesture {
func didTap() {
    print("tap button")
}
func didLongPress() {
    print("long press")
}
func didSwipe() {
    print("swipe")
}
}

class PoorButton_SOLID: TapGesture {
func didTap() {
    print("tap button")
}
}

Такое разделение позволяет классам реализовывать только необходимые им интерфейсы, что упрощает тестирование и уменьшает связанность кода.

Принцип инверсии зависимостей (Dependency Inversion Principle, DIP)

Определение и значение

DIP постулирует, что модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций. В контексте iOS-разработки это означает использование протоколов и внедрения зависимостей для уменьшения связанности компонентов.

Примеры нарушения DIP в iOS

class SaveData_NOT_SOLID {
let filesSystemManager = FilesSystemManager_NOT_SOLID()
   
func handle(data: String) {
    filesSystemManager.save(data: data)
}
}

class FilesSystemManager_NOT_SOLID {
func save(data: String) {
    // Логика сохранения
}
}

Здесь высокоуровневый класс SaveData жестко связан с конкретной реализацией FilesSystemManager.

Применение DIP в iOS

protocol Storage {
func save(data: Any)
}

class SaveData_SOLID {
let storage: Storage
   
init(storage: Storage) {
    self.storage = storage
}
   
func handle(data: Any) {
    storage.save(data: data)
}
}

class FilesSystemManager_SOLID: Storage {
func save(data: Any) {
    // Реализация сохранения в файловую систему
}
}

class CloudStorage: Storage {
func save(data: Any) {
    // Реализация сохранения в облако
}
}

Такая архитектура позволяет легко менять способ хранения данных и упрощает тестирование через внедрение mock-объектов.

Преимущества применения принципов SOLID в iOS-разработке

Внедрение принципов SOLID в iOS-разработку приносит ряд существенных преимуществ, которые особенно заметны в долгосрочной перспективе:

  • Улучшение тестируемости
  1. Изолированные компоненты легче покрывать unit-тестами
  2. Возможность использования mock-объектов через протоколы
  3. Упрощение интеграционного тестирования
  • Повышение поддерживаемости
  1. Локализация изменений в конкретных модулях
  2. Уменьшение риска побочных эффектов при модификациях
  3. Более простая отладка и поиск ошибок
  • Масштабируемость
  1. Легкое добавление новой функциональности
  2. Возможность параллельной работы команды
  3. Упрощение рефакторинга
  • Улучшение архитектуры
  1. Четкое разделение ответственности между компонентами
  2. Снижение связанности модулей
  3. Повышение переиспользуемости кода

Соблюдение этих принципов становится особенно важным при разработке крупных iOS-приложений, где качество архитектуры напрямую влияет на скорость разработки и стоимость поддержки продукта.

Заключение

В современной iOS-разработке принципы SOLID представляют собой не просто теоретическую концепцию, а практический инструментарий для создания качественного программного обеспечения. Как мы убедились на конкретных примерах, каждый из принципов помогает решать определенные архитектурные проблемы:

  • SRP способствует созданию модульной архитектуры с четким разделением ответственности
  • OCP обеспечивает гибкость при добавлении новой функциональности
  • LSP гарантирует корректность иерархий и полиморфного поведения
  • ISP помогает создавать узкоспециализированные интерфейсы
  • DIP снижает связанность компонентов системы

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

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

Читайте также
Категории курсов
Отзывы о школах