🍎 Swift

POO: Encapsulamento e Polimorfismo em Swift: Um Guia Detalhado

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты собираешься в школу. У тебя есть рюкзак. Внутри рюкзака лежат учебники, тетради, пенал с ручками и бутерброд. Ты не показываешь содержимое рюкзака всем прохожим — это твои личные вещи. Но ты можешь достать учебник, когда нужно сделать уроки, или положить новый дневник. Это и есть инкапсуляция: ты скрываешь внутреннее устройство (что именно лежит в рюкзаке), но предоставляешь способы взаимодействия (открыть рюкзак, достать книгу).

Теперь представь, что у тебя есть домашние животные: кошка, собака и попугай. Ты говоришь команду «Голос!». Кошка мяукает, собака лает, попугай чирикает. Одна и та же команда вызывает разное поведение у разных животных. Это полиморфизм: один интерфейс (команда «Голос!») работает по-разному в зависимости от того, кто его получает.

💡 Главная идея: Инкапсуляция защищает данные от случайной порчи, а полиморфизм позволяет писать гибкий код, который работает с разными типами объектов единообразно.

Теория: как это работает изнутри

Инкапсуляция

В программировании инкапсуляция — это объединение данных (свойств) и методов (функций), которые с ними работают, внутри одного класса или структуры. При этом доступ к внутренним данным ограничивается с помощью модификаторов доступа.

В Swift есть три уровня доступа:

МодификаторЧто означаетПример из жизни
privateДоступно только внутри этого классаТвой личный дневник — никто не может в него заглянуть
internalДоступно внутри всего модуля (по умолчанию)Школьный журнал — видят учителя твоей школы
publicДоступно всемОбъявление в газете — видят все

Когда мы делаем свойство private, мы говорим: «Никто снаружи не может изменить это напрямую». Вместо этого мы создаём методы (геттеры и сеттеры), которые проверяют, можно ли изменить значение, и только потом меняют его.

Полиморфизм

Полиморфизм (от греч. «много форм») — это способность объектов разных классов отвечать на одни и те же вызовы методов по-своему.

В Swift полиморфизм реализуется через:

  • Наследование — дочерний класс переопределяет метод родителя
  • Протоколы — разные классы реализуют один и тот же протокол
  • Перегрузку методов — одно и то же имя метода, но разные параметры
⚠️ Важно: Полиморфизм работает только тогда, когда мы используем тип родительского класса или протокола для хранения объектов разных дочерних классов.

Синтаксис в Swift: все формы и варианты

Инкапсуляция: синтаксис

// 1. Простая инкапсуляция с private
class BankAccount {
    private var balance: Double = 0.0  // Только внутри класса!
    
    func deposit(amount: Double) {
        if amount > 0 {
            balance += amount
        }
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// 2. Инкапсуляция с вычисляемыми свойствами
class Thermometer {
    private var celsius: Double = 0.0
    
    var fahrenheit: Double {
        get { return celsius * 9/5 + 32 }
        set { celsius = (newValue - 32) * 5/9 }
    }
}

// 3. Инкапсуляция с наблюдателями
class Student {
    private var _name: String = ""
    var name: String {
        get { return _name }
        set {
            if !newValue.isEmpty {
                _name = newValue
            }
        }
    }
}

Полиморфизм: синтаксис

// 1. Через наследование
class Animal {
    func makeSound() {
        print("Животное издаёт звук")
    }
}

class Dog: Animal {
    override func makeSound() {
        print("Гав-гав!")
    }
}

// 2. Через протоколы
protocol Drawable {
    func draw()
}

class Circle: Drawable {
    func draw() {
        print("Рисуем круг")
    }
}

class Square: Drawable {
    func draw() {
        print("Рисуем квадрат")
    }
}

// 3. Перегрузка методов
class Calculator {
    func add(_ a: Int, _ b: Int) -> Int { a + b }
    func add(_ a: Double, _ b: Double) -> Double { a + b }
    func add(_ a: String, _ b: String) -> String { a + b }
}

Примеры кода — от простого к сложному

Пример 1: Простейшая инкапсуляция

// Создаём класс "Кошелёк"
class Wallet {
    // Приватное свойство — никто не может изменить деньги напрямую
    private var money: Int = 0
    
    // Публичный метод для добавления денег
    func addMoney(amount: Int) {
        if amount > 0 {
            money += amount
            print("Добавлено (amount) рублей. Баланс: (money)")
        }
    }
    
    // Публичный метод для траты денег
    func spendMoney(amount: Int) -> Bool {
        if amount > 0 && amount <= money {
            money -= amount
            print("Потрачено (amount) рублей. Осталось: (money)")
            return true
        }
        print("Недостаточно средств!")
        return false
    }
    
    // Публичный метод для просмотра баланса
    func checkBalance() -> Int {
        return money
    }
}

// Используем наш кошелёк
let myWallet = Wallet()
myWallet.addMoney(amount: 1000)  // Добавлено 1000 рублей
myWallet.spendMoney(amount: 300) // Потрачено 300 рублей
print("Баланс: (myWallet.checkBalance())") // Баланс: 700
// myWallet.money = 9999 — ОШИБКА! money приватное

Пример 2: Инкапсуляция с проверкой данных

// Класс "Пользователь" с защитой данных
class User {
    // Приватное хранилище для возраста
    private var _age: Int = 0
    
    // Вычисляемое свойство с проверкой
    var age: Int {
        get {
            return _age
        }
        set {
            // Проверяем, что возраст реалистичный
            if newValue >= 0 && newValue <= 150 {
                _age = newValue
            } else {
                print("Ошибка: возраст должен быть от 0 до 150")
            }
        }
    }
    
    // Приватное хранилище для email
    private var _email: String = ""
    
    var email: String {
        get {
            return _email
        }
        set {
            // Простейшая проверка: email должен содержать @
            if newValue.contains("@") {
                _email = newValue
            } else {
                print("Ошибка: неверный формат email")
            }
        }
    }
    
    // Конструктор
    init(age: Int, email: String) {
        self.age = age    // Используем сеттер с проверкой
        self.email = email
    }
}

let user = User(age: 25, email: "test@mail.ru")
print("Возраст: (user.age)")  // Возраст: 25
user.age = -5  // Ошибка: возраст должен быть от 0 до 150
user.email = "bad-email"  // Ошибка: неверный формат email

Пример 3: Базовый полиморфизм через наследование

// Базовый класс "Транспорт"
class Transport {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    // Метод, который будут переопределять
    func move() {
        print("(name) движется")
    }
}

// Дочерний класс "Машина"
class Car: Transport {
    override func move() {
        print("(name) едет по дороге")
    }
}

// Дочерний класс "Лодка"
class Boat: Transport {
    override func move() {
        print("(name) плывёт по воде")
    }
}

// Дочерний класс "Самолёт"
class Airplane: Transport {
    override func move() {
        print("(name) летит в небе")
    }
}

// Демонстрация полиморфизма
let vehicles: [Transport] = [
    Car(name: "Toyota"),
    Boat(name: "Яхта"),
    Airplane(name: "Боинг")
]

for vehicle in vehicles {
    vehicle.move()  // Один вызов — разное поведение!
}
// Вывод:
// Toyota едет по дороге
// Яхта плывёт по воде
// Боинг летит в небе

Пример 4: Полиморфизм через протоколы

// Определяем протокол "Платежная система"
protocol PaymentSystem {
    var name: String { get }
    func pay(amount: Double) -> Bool
    func refund(amount: Double) -> Bool
}

// Класс "Банковская карта"
class CreditCard: PaymentSystem {
    var name: String = "Visa"
    private var limit: Double = 100000
    
    func pay(amount: Double) -> Bool {
        if amount <= limit {
            limit -= amount
            print("(name): оплачено (amount) руб. Остаток лимита: (limit)")
            return true
        }
        print("(name): превышен лимит!")
        return false
    }
    
    func refund(amount: Double) -> Bool {
        limit += amount
        print("(name): возвращено (amount) руб. Лимит: (limit)")
        return true
    }
}

// Класс "Электронный кошелёк"
class ElectronicWallet: PaymentSystem {
    var name: String = "YooMoney"
    private var balance: Double = 5000
    
    func pay(amount: Double) -> Bool {
        if amount <= balance {
            balance -= amount
            print("(name): оплачено (amount) руб. Баланс: (balance)")
            return true
        }
        print("(name): недостаточно средств!")
        return false
    }
    
    func refund(amount: Double) -> Bool {
        balance += amount
        print("(name): возвращено (amount) руб. Баланс: (balance)")
        return true
    }
}

// Используем полиморфизм
let paymentMethods: [PaymentSystem] = [CreditCard(), ElectronicWallet()]

for method in paymentMethods {
    method.pay(amount: 3000)  // Один интерфейс — разная реализация
}
// Вывод:
// Visa: оплачено 3000 руб. Остаток лимита: 97000
// YooMoney: оплачено 3000 руб. Баланс: 2000

Пример 5: Комбинация инкапсуляции и полиморфизма

// Протокол "Фигура"
protocol Shape {
    var name: String { get }
    func area() -> Double
    func perimeter() -> Double
}

// Класс "Прямоугольник" с инкапсуляцией
class Rectangle: Shape {
    var name: String = "Прямоугольник"
    
    // Приватные стороны
    private var width: Double
    private var height: Double
    
    init(width: Double, height: Double) {
        self.width = max(0, width)   // Защита от отрицательных значений
        self.height = max(0, height)
    }
    
    // Публичные методы для изменения сторон с проверкой
    func setWidth(_ newWidth: Double) {
        width = max(0, newWidth)
    }
    
    func setHeight(_ newHeight: Double) {
        height = max(0, newHeight)
    }
    
    // Реализация протокола
    func area() -> Double {
        return width * height
    }
    
    func perimeter() -> Double {
        return 2 * (width + height)
    }
}

// Класс "Круг" с инкапсуляцией
class Circle: Shape {
    var name: String = "Круг"
    
    private var radius: Double
    
    init(radius: Double) {
        self.radius = max(0, radius)
    }
    
    func setRadius(_ newRadius: Double) {
        radius = max(0, newRadius)
    }
    
    func area() -> Double {
        return Double.pi * radius * radius
    }
    
    func perimeter() -> Double {
        return 2 * Double.pi * radius
    }
}

// Демонстрация
let shapes: [Shape] = [
    Rectangle(width: 5, height: 3),
    Circle(radius: 4)
]

for shape in shapes {
    print("(shape.name): площадь = (shape.area()), периметр = (shape.perimeter())")
}
// Вывод:
// Прямоугольник: площадь = 15.0, периметр = 16.0
// Круг: площадь = 50.265..., периметр = 25.132...

Пример 6: Инкапсуляция с ленивыми свойствами

// Класс "База данных" с ленивой загрузкой
class DatabaseManager {
    // Приватное ленивое свойство — создаётся только при первом обращении
    private lazy var connection: DatabaseConnection = {
        print("Создаём подключение к базе данных...")
        return DatabaseConnection()
    }()
    
    private var isConnected: Bool = false
    
    func connect() {
        // При обращении к connection, оно создаётся автоматически
        connection.open()
        isConnected = true
        print("Подключение установлено")
    }
    
    func disconnect() {
        if isConnected {
            connection.close()
            isConnected = false
            print("Подключение закрыто")
        }
    }
}

class DatabaseConnection {
    func open() {
        print("Открываем соединение...")
    }
    
    func close() {
        print("Закрываем соединение...")
    }
}

let db = DatabaseManager()
// Подключение ещё не создано!
db.connect()  // Теперь создаётся и открывается
// Создаём подключение к базе данных...
// Открываем соединение...
// Подключение установлено

Пример 7: Полиморфизм с дженериками

// Протокол "Сравнимый"
protocol Comparable {
    func isGreater(than other: Self) -> Bool
}

// Класс "Студент" реализует протокол
class Student: Comparable {
    var name: String
    private var grade: Int
    
    init(name: String, grade: Int) {
        self.name = name
        self.grade = grade
    }
    
    func isGreater(than other: Student) -> Bool {
        return self.grade > other.grade
    }
}

// Класс "Книга" тоже реализует протокол
class Book: Comparable {
    var title: String
    private var pages: Int
    
    init(title: String, pages: Int) {
        self.title = title
        self.pages = pages
    }
    
    func isGreater(than other: Book) -> Bool {
        return self.pages > other.pages
    }
}

// Функция с дженериком — работает с любым типом, реализующим Comparable
func findMax(_ a: T, _ b: T) -> T {
    return a.isGreater(than: b) ? a : b
}

let student1 = Student(name: "Анна", grade: 85)
let student2 = Student(name: "Иван", grade: 92)
let bestStudent = findMax(student1, student2)
print("Лучший студент: (bestStudent.name)")  // Лучший студент: Иван

let book1 = Book(title: "Война и мир", pages: 1225)
let book2 = Book(title: "Маленький принц", pages: 96)
let biggerBook = findMax(book1, book2)
print("Книга с большим количеством страниц: (biggerBook.title)")  // Война и мир

Сравнение с Python: чем похоже и чем отличается

АспектSwiftPython
ИнкапсуляцияСтрогая: private, internal, publicСоглашение: _приватное и __сильно_приватное (name mangling)
ПолиморфизмЧерез протоколы и наследование (строгая типизация)Утиная типизация (duck typing) — если объект имеет метод, он подходит
Перегрузка методовПоддерживается (разные типы параметров)Не поддерживается напрямую (используют *args, **kwargs)
Протоколы/ИнтерфейсыПротоколы с обязательными и опциональными методамиАбстрактные базовые классы (ABC) из модуля abc
Вычисляемые свойстваЕсть (get/set)Есть (@property)
Наблюдатели свойствЕсть (willSet/didSet)Нет встроенных (используют property + setter)
Ленивые свойстваЕсть (lazy var)Нет встроенных (используют декораторы или __getattr__)
💡 Ключевое отличие: Swift — статически типизированный язык, поэтому полиморфизм проверяется на этапе компиляции. Python — динамический, поэтому полиморфизм работает во время выполнения и более гибкий, но менее безопасный.

Типичные ошибки и подводные камни

Ошибка 1: Забыли про инкапсуляцию

// ❌ ПЛОХО: все свойства открыты
class BankAccount {
    var balance: Double = 0  // Любой может изменить!
}

let account = BankAccount()
account.balance = -1000  // Отрицательный баланс — это проблема!

// ✅ ХОРОШО: инкапсулируем
class BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        if amount > 0 { balance += amount }
    }
}

Ошибка 2: Неправильное использование self

class Person {
    var name: String
    
    init(name: String) {
        // ❌ ОШИБКА: name = name — присваивание самому себе
        // name = name  // Не работает!
        
        // ✅ ПРАВИЛЬНО: self.name = name
        self.name = name
    }
}

Ошибка 3: Забыли override

class Animal {
    func speak() {
        print("...")
    }
}

class Dog: Animal {
    // ❌ ОШИБКА: без override это новый метод, не переопределение
    func speak() {
        print("Гав!")
    }
}

// ✅ ПРАВИЛЬНО:
class Dog: Animal {
    override func speak() {
        print("Гав!")
    }
}

Ошибка 4: Путаница с протоколами

protocol Flyable {
    func fly()
}

// ❌ ОШИБКА: класс не реализовал все методы протокола
class Bird: Flyable {
    // Забыли реализовать fly()
}

// ✅ ПРАВИЛЬНО:
class Bird: Flyable {
    func fly() {
        print("Птица летит")
    }
}

Ошибка 5: Нарушение принципа подстановки Лисков

class Rectangle {
    var width: Double
    var height: Double
    
    func setWidth(_ w: Double) { width = w }
    func setHeight(_ h: Double) { height = h }
    func area() -> Double { width * height }
}

// ❌ ПЛОХО: квадрат нарушает поведение прямоугольника
class Square: Rectangle {
    override func setWidth(_ w: Double) {
        super.setWidth(w)
        super.setHeight(w)  // Меняет и высоту!
    }
}

// Теперь код, работающий с Rectangle, сломается для Square
let rect: Rectangle = Square()
rect.setWidth(5)
rect.setHeight(10)
print(rect.area())  // Ожидаем 50, получим 100!

Лучшие практики и советы профессионалов

  1. Начинайте с private — делайте все свойства приватными по умолчанию. Открывайте доступ только когда это действительно нужно.
  2. Используйте протоколы для полиморфизма — они гибче, чем наследование, и позволяют объектам быть частью разных групп.
  3. Не злоупотребляйте наследованием — глубокие иерархии классов сложно поддерживать. Предпочитайте композицию.
  4. Пишите тесты — инкапсуляция и полиморфизм легче тестируются, когда код хорошо структурирован.
  5. Используйте value semantics (структуры) — в Swift структуры с их копированием при присваивании дают инкапсуляцию «из коробки».
  6. Документируйте протоколы — каждый протокол должен иметь комментарий, объясняющий, что он делает.
  7. Избегайте «божественных объектов» — класс не должен знать всё и делать всё. Разделяйте ответственность.
🔥 Золотое правило: Программируйте на уровне интерфейсов (протоколов), а не реализаций. Это делает код гибким и легко расширяемым.

Вопросы для самопроверки

  1. Что такое инкапсуляция и зачем она нужна?
    Ответ: Это сокрытие внутренних данных объекта и предоставление контролируемого доступа через методы. Нужна для защиты данных от некорректного использования.
  2. Какие модификаторы доступа есть в Swift?
    Ответ: private (только внутри класса), internal (внутри модуля, по умолчанию), public (доступно всем).
  3. Что такое полиморфизм?
    Ответ: Способность объектов разных классов отвечать на одни и те же вызовы методов по-разному.
  4. Как реализовать полиморфизм в Swift?
    Ответ: Через наследование (переопределение методов), протоколы (реализация требований) и перегрузку методов.
  5. В чём разница между протоколом и базовым классом?
    Ответ: Протокол определяет только интерфейс (какие методы должны быть), но не реализацию. Класс может реализовать много протоколов, но наследоваться только от одного класса.
  6. Что такое вычисляемое свойство?
    Ответ: Свойство, которое не хранит значение, а вычисляет его при каждом обращении через getter и setter.
  7. Как защитить данные от некорректных значений?
    Ответ: Использовать приватные свойства с публичными методами-сеттерами, которые проверяют входные данные.
  8. Что такое lazy var и когда его использовать?
    Ответ: Ленивое свойство создаётся только при первом обращении. Используется для ресурсоёмких объектов, которые могут не понадобиться.
  9. В чём отличие инкапсуляции в Swift и Python?
    Ответ: В Swift инкапсуляция строгая (компилятор не даст обратиться к private свойству), в Python — на уровне соглашений (подчёркивания).
  10. Почему не стоит делать все свойства public?
    Ответ: Это нарушает инкапсуляцию, позволяет изменить данные без проверок, что может привести к некорректному состоянию объекта.

Итог и что изучить дальше

Мы разобрали два фундаментальных принципа ООП: инкапсуляцию — защиту данных через сокрытие, и полиморфизм — способность объектов разных типов работать через единый интерфейс. В Swift эти механизмы реализованы особенно элегантно благодаря протоколам, строгой типизации и мощным модификаторам доступа.

Теперь ты знаешь:

  • Как защищать данные с помощью private и вычисляемых свойств
  • Как создавать гибкие системы с помощью протоколов и наследования
  • Как избегать типичных ошибок новичков
  • Как применять лучшие практики в реальных проектах

Что изучать дальше:

  1. Наследование — как создавать иерархии классов, ключевое слово super, предотвращение переопределения (final).
  2. Протоколы и расширения — как добавлять методы к существующим типам, композиция протоколов.
  3. Дженерики — как писать универсальный код, работающий с любыми типами.
  4. ARC и управление памятью — как Swift управляет памятью, циклы сильных ссылок.
  5. Паттерны проектирования — Singleton, Factory, Delegate — как применять ООП на практике.
🎯 Совет на будущее: Лучший способ закрепить теорию — написать свой небольшой проект. Попробуй создать симуляцию зоопарка (животные, их звуки, кормление) или простую банковскую систему (счета, транзакции, проценты). Применяй инкапсуляцию и полиморфизм — и ты увидишь, как код становится чистым и надёжным!