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: чем похоже и чем отличается
| Аспект | Swift | Python |
|---|---|---|
| Инкапсуляция | Строгая: 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!
Лучшие практики и советы профессионалов
- Начинайте с private — делайте все свойства приватными по умолчанию. Открывайте доступ только когда это действительно нужно.
- Используйте протоколы для полиморфизма — они гибче, чем наследование, и позволяют объектам быть частью разных групп.
- Не злоупотребляйте наследованием — глубокие иерархии классов сложно поддерживать. Предпочитайте композицию.
- Пишите тесты — инкапсуляция и полиморфизм легче тестируются, когда код хорошо структурирован.
- Используйте value semantics (структуры) — в Swift структуры с их копированием при присваивании дают инкапсуляцию «из коробки».
- Документируйте протоколы — каждый протокол должен иметь комментарий, объясняющий, что он делает.
- Избегайте «божественных объектов» — класс не должен знать всё и делать всё. Разделяйте ответственность.
🔥 Золотое правило: Программируйте на уровне интерфейсов (протоколов), а не реализаций. Это делает код гибким и легко расширяемым.
Вопросы для самопроверки
- Что такое инкапсуляция и зачем она нужна?
Ответ: Это сокрытие внутренних данных объекта и предоставление контролируемого доступа через методы. Нужна для защиты данных от некорректного использования. - Какие модификаторы доступа есть в Swift?
Ответ:private(только внутри класса),internal(внутри модуля, по умолчанию),public(доступно всем). - Что такое полиморфизм?
Ответ: Способность объектов разных классов отвечать на одни и те же вызовы методов по-разному. - Как реализовать полиморфизм в Swift?
Ответ: Через наследование (переопределение методов), протоколы (реализация требований) и перегрузку методов. - В чём разница между протоколом и базовым классом?
Ответ: Протокол определяет только интерфейс (какие методы должны быть), но не реализацию. Класс может реализовать много протоколов, но наследоваться только от одного класса. - Что такое вычисляемое свойство?
Ответ: Свойство, которое не хранит значение, а вычисляет его при каждом обращении через getter и setter. - Как защитить данные от некорректных значений?
Ответ: Использовать приватные свойства с публичными методами-сеттерами, которые проверяют входные данные. - Что такое lazy var и когда его использовать?
Ответ: Ленивое свойство создаётся только при первом обращении. Используется для ресурсоёмких объектов, которые могут не понадобиться. - В чём отличие инкапсуляции в Swift и Python?
Ответ: В Swift инкапсуляция строгая (компилятор не даст обратиться к private свойству), в Python — на уровне соглашений (подчёркивания). - Почему не стоит делать все свойства public?
Ответ: Это нарушает инкапсуляцию, позволяет изменить данные без проверок, что может привести к некорректному состоянию объекта.
Итог и что изучить дальше
Мы разобрали два фундаментальных принципа ООП: инкапсуляцию — защиту данных через сокрытие, и полиморфизм — способность объектов разных типов работать через единый интерфейс. В Swift эти механизмы реализованы особенно элегантно благодаря протоколам, строгой типизации и мощным модификаторам доступа.
Теперь ты знаешь:
- Как защищать данные с помощью private и вычисляемых свойств
- Как создавать гибкие системы с помощью протоколов и наследования
- Как избегать типичных ошибок новичков
- Как применять лучшие практики в реальных проектах
Что изучать дальше:
- Наследование — как создавать иерархии классов, ключевое слово super, предотвращение переопределения (final).
- Протоколы и расширения — как добавлять методы к существующим типам, композиция протоколов.
- Дженерики — как писать универсальный код, работающий с любыми типами.
- ARC и управление памятью — как Swift управляет памятью, циклы сильных ссылок.
- Паттерны проектирования — Singleton, Factory, Delegate — как применять ООП на практике.
🎯 Совет на будущее: Лучший способ закрепить теорию — написать свой небольшой проект. Попробуй создать симуляцию зоопарка (животные, их звуки, кормление) или простую банковскую систему (счета, транзакции, проценты). Применяй инкапсуляцию и полиморфизм — и ты увидишь, как код становится чистым и надёжным!