JavaRush /Курсы /Swift SELF /Инкапсуляция: скрываем детали реализации

Инкапсуляция: скрываем детали реализации

Swift SELF
49 уровень , 4 лекция
Открыта

1. Зачем нужна инкапсуляция

Когда вы пишете учебный проект, очень легко попасть в ловушку «лишь бы сейчас заработало». И оно действительно заработает — ровно до того момента, пока вы не захотите изменить формат хранения, добавить правило «ID уникален», или подключить другой модуль. Инкапсуляция — это способ заранее договориться: что мы считаем публичным контрактом, а что — «кухонными тайнами», которые можно менять без боли.

Представьте LibraryCLI как маленькую библиотеку (в смысле «место с книгами», а не «пакет Swift»). У неё есть понятные действия: добавить книгу, удалить книгу, найти книгу, вывести список. Пользователю библиотеки не нужно знать, лежат ли книги в массиве, словаре или в коробке из-под печенья — ему нужно, чтобы команды работали предсказуемо.

“Меню” и “кухня”: модель публичного API

Инкапсуляция легче всего понимается через бытовую аналогию. В ресторане вам дают меню: «пицца, паста, кофе». Это и есть публичный API. А вот как именно на кухне жарят, солят и моют посуду — это детали реализации. Клиент не должен зависеть от того, какой марки у повара сковородка, иначе любое обновление кухни сломает «контракт» (и клиент начнёт требовать «верните старую сковородку обратно»).

В Swift роль «меню» играют public/internal свойства и методы. Роль «кухни» играют private/fileprivate детали: хранилища, вспомогательные функции, промежуточные структуры. И ключевой момент: private в Swift — это лексическая область объявления, а не «просто внутри файла где-то рядом».

Давайте сразу посмотрим на антипример — «библиотека без кухни, всё на виду».

public struct Library {
    public var books: [String] = []
}

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

2. Скрываем хранение и поддерживаем инварианты

В нашем LibraryCLI у нас есть модуль Domain, который описывает предметную область: книги, идентификаторы, каталог. Командный модуль LibraryCLI должен уметь пользоваться доменом, но не должен знать, как именно домен хранит данные внутри.

Именно тут инкапсуляция становится практикой: мы прячем хранилище и выдаём наружу только операции. В Swift это обычно выглядит как private var хранилище и набор public/internal методов поверх него.

Начнём с мини-моделей (упрощённо), которые можно положить в Sources/Domain/.

public struct BookID: Hashable {
    public let rawValue: String

    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}

public struct Book {
    public let id: BookID
    public let title: String

    public init(id: BookID, title: String) {
        self.id = id
        self.title = title
    }
}

А теперь — сам каталог, но уже с инкапсуляцией:

public struct LibraryCatalog {
    private var booksByID: [BookID: Book] = [:]

    public init() {}

    public var count: Int {
        booksByID.count
    }
}

Мы спрятали booksByID. Снаружи теперь нельзя «случайно» сделать catalog.booksByID.removeAll(). Это и есть минимальный, но очень важный шаг к предсказуемости.

Почему private именно такой силы? Потому что private в Swift означает «видно только внутри текущего объявления (и связанных extensions того же типа в этом файле)», то есть это реально помогает скрывать внутренности, а не просто «делать вид».

Инварианты: правила жизни типа

Когда мы делаем тип, который хранит состояние, мы почти всегда имеем набор правил, которые нельзя нарушать. Эти правила называют инвариантами. В нашем каталоге инвариант очень понятный: книга с одинаковым BookID не должна существовать два раза. Если мы отдадим наружу массив и скажем «ну вы там аккуратно», это будет не инкапсуляция, а надежда.

Поэтому вместо «открытого массива» делаем методы, которые единственные имеют право менять хранилище. И внутри этих методов поддерживаем инварианты.

public struct LibraryCatalog {
    private var booksByID: [BookID: Book] = [:]

    public init() {}

    public mutating func add(_ book: Book) -> Bool {
        guard booksByID[book.id] == nil else { return false }
        booksByID[book.id] = book
        return true
    }
}

Обратите внимание на мелочь, которая спасает нервы: метод возвращает Bool. Мы пока не углубляемся в дизайн ошибок и сообщений — здесь цель в другом: наружу нельзя сломать уникальность, даже если кто-то очень старается.

Теперь добавим чтение — но тоже аккуратно. Мы можем выдать список книг, но в форме, которая не даёт менять внутренности напрямую.

public struct LibraryCatalog {
    private var booksByID: [BookID: Book] = [:]

    public var allBooks: [Book] {
        Array(booksByID.values)
    }
}

Да, это создаёт новый массив — зато пользователь не получает «ручку управления» нашим словарём. Это классический компромисс инкапсуляции: иногда мы платим небольшую цену за копирование, чтобы защитить контракт.

public private(set): чтение наружу, запись через методы

Очень частый сценарий: вы хотите, чтобы внешний код мог видеть значение, но не мог менять его напрямую. Например, мы хотим показывать наружу «сколько успешных добавлений было сделано», но увеличивать счётчик — только в add.

Swift даёт для этого отдельный инструмент: модификатор доступа на сеттере, то есть private(set). Это не «магия», а осознанная часть модели access control: чтение и запись можно ограничивать по-разному.

Добавим счётчик:

public struct LibraryCatalog {
    private var booksByID: [BookID: Book] = [:]
    public private(set) var successfulAdds: Int = 0

    public mutating func add(_ book: Book) -> Bool {
        guard booksByID[book.id] == nil else { return false }
        booksByID[book.id] = book
        successfulAdds += 1
        return true
    }
}

Теперь снаружи можно написать:

var catalog = LibraryCatalog()
print(catalog.successfulAdds) // 0

Но нельзя:

// catalog.successfulAdds = 999 // ошибка: сеттер private

И это ровно то, что нам нужно: наружу — «телеметрия», внутрь — контроль.

3. Контроль создания объектов

Иногда вам нужно сделать тип видимым снаружи (public), но запретить создавать его «как попало». Это особенно полезно для сущностей с правилами, которые вы хотите держать под контролем: токены, идентификаторы, нормализованные строки.

В Swift это делается очень просто: у public типа вы делаете инициализатор не public, а оставляете его по умолчанию (internal) или делаете private, а наружу даёте фабрику static func ....

Смысл в том, что «тип существует», но «создаётся только правильным способом». Это прямое продолжение идеи инкапсуляции: вы прячете не только поля, но и способ появления корректного значения.

Пример для BookID: пусть мы хотим, чтобы BookID создавался только из «вменяемого» ввода (обрезали пробелы и запретили пустоту). Для этого init делаем внутренним, а наружу даём fromUserInput.

import Foundation

public struct BookID: Hashable {
    public let rawValue: String

    init(rawValue: String) {              // internal init
        self.rawValue = rawValue
    }

    public static func fromUserInput(_ text: String) -> BookID? {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return nil }
        return BookID(rawValue: trimmed)
    }
}

Обратите внимание на тонкую, но важную логику: внешний код не может сделать BookID(rawValue: ""), потому что init не публичный. Он обязан пройти через «входную дверь» fromUserInput.

4. Организация кода: extensions и границы модулей

Когда проект растёт, даже один тип начинает выглядеть как «простыня»: свойства, методы, хелперы, форматирование, внутренние функции. Инкапсуляция — это ещё и про то, чтобы человек, открывший файл, сразу видел, где «витрина», а где «подсобка».

Swift позволяет организовывать это через extension. При этом у extensions есть свои правила доступа: модификатор на extension задаёт дефолтный доступ для членов внутри него, и это можно использовать как инструмент структуры файла.

Extensions: “витрина” и “подсобка”

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

import Foundation

public struct LibraryCatalog {
    private var booksByID: [BookID: Book] = [:]
    public init() {}
}

public extension LibraryCatalog {
    func containsTitle(_ text: String) -> Bool {
        let needle = normalize(text)
        return booksByID.values.contains { normalize($0.title) == needle }
    }
}

private extension LibraryCatalog {
    func normalize(_ text: String) -> String {
        text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
    }
}

Здесь очень приятный эффект: когда другой разработчик (или вы через две недели) набирает catalog. в автодополнении, он видит «меню»: containsTitle, add, count и т.д. А normalize туда не вылезает и не провоцирует использовать его «потому что удобно».

И да, private в таких сценариях — не косметика, а строгая граница: «нельзя снаружи».

Инкапсуляция между модулями

Теперь давайте посмотрим на инкапсуляцию чуть шире: в SwiftPM границы видимости — это не только private, но и граница модуля. По умолчанию internal означает «видно только внутри target’а». Это позволяет держать половину кухни вообще за стеной, которую внешний код не пробьёт.

У нас типичная структура курса: LibraryCLI (executable) импортирует Domain (library target). В этой связке очень полезно держать правило: Domain показывает наружу только то, чем реально должны пользоваться команды CLI.

Если мы сделаем всё public, то любой код в LibraryCLI сможет:

  • начать зависеть от внутренностей домена,
  • потом вы не сможете поменять реализацию без массового рефакторинга,
  • а ошибки будут выглядеть как «сломали всё» вместо «изменилась внутренняя кухня».

Поэтому в Domain часто получается такая стратегия:

  • публичные типы: Book, BookID, LibraryCatalog (то, что реально нужно потребителю),
  • скрытые детали: приватные хелперы, внутренние структуры индекса, внутренние функции нормализации,
  • методы, которые поддерживают инварианты: публичные или internal, но не «открытые поля».

Это и есть инкапсуляция «по слоям»: часть прячем на уровне private, часть — на уровне internal (внутри модуля), и совсем небольшую часть показываем как public.

5. Мини-рефакторинг каталога

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

public struct LibraryCatalog {
    public var books: [Book] = []
}

А где-то в LibraryCLI (в исполняемом модуле) вы делали:

var catalog = LibraryCatalog()
catalog.books.append(Book(id: BookID(rawValue: "1"), title: "Swift"))
catalog.books.append(Book(id: BookID(rawValue: "1"), title: "Swift 2")) // дубликат
catalog.books.removeAll() // “ой”

Это не «плохой разработчик» — это нормальный человек, которого не остановили границы.

Теперь версия с инкапсуляцией. В Domain:

public struct LibraryCatalog {
    private var booksByID: [BookID: Book] = [:]

    public init() {}

    public mutating func add(_ book: Book) -> Bool {
        guard booksByID[book.id] == nil else { return false }
        booksByID[book.id] = book
        return true
    }

    public func find(by id: BookID) -> Book? {
        booksByID[id]
    }
}

А в LibraryCLI вы вынуждены использовать контракт:

var catalog = LibraryCatalog()

let ok1 = catalog.add(Book(id: BookID(rawValue: "1"), title: "Swift"))
let ok2 = catalog.add(Book(id: BookID(rawValue: "1"), title: "Swift 2"))

print(ok1) // true
print(ok2) // false

И вот это «вынуждены» — на самом деле комплимент архитектуре. Инкапсуляция делает правильный путь простым, а неправильный — невозможным.

6. Типичные ошибки

Ошибка №1: “Сделаю свойства публичными, чтобы было проще”.
Обычно это происходит не из злого умысла, а из желания быстрее написать код в CLI: «ну мне же надо добавить книгу, зачем мне метод». Проблема в том, что публичное свойство — это вечная дверь в вашу кухню. Как только внешний код начал напрямую менять books, вы больше не можете гарантировать уникальность, сортировку, корректность счётчиков и вообще любые правила. В результате тип перестаёт быть «умным» и становится просто мешком данных.

Ошибка №2: Инкапсуляция только “на словах”, без поддержки компилятора.
Иногда пытаются договориться комментарием: «не трогайте это поле», «используйте метод». Но Swift-компилятор не читает комментарии, он читает модификаторы доступа. Если поле не должно использоваться снаружи, оно должно быть private (или хотя бы internal внутри модуля), иначе это не правило, а пожелание.

Ошибка №3: Злоупотребление fileprivate вместо аккуратного private.
fileprivate кажется удобным: «пусть весь файл видит всё». Но постепенно файл разрастается, туда добавляются новые типы, и внезапно они начинают пользоваться чужими внутренностями просто потому, что «могут». Лучше начинать с private (граница объявления) и расширять доступ только когда действительно появляется необходимость делить хелперы на уровне файла. Разница между private и fileprivate как раз про это: один прячет внутри объявления, другой открывает всему файлу.

Ошибка №4: Забыть, что private(set) — это тоже контракт.
public private(set) — прекрасный инструмент, но он работает только если вы действительно меняете значение строго через методы типа, поддерживая инварианты. Если внутри типа вы начинаете менять такие свойства хаотично, или делаете несколько способов записи, внешний код получает «мигающую реальность»: сегодня successfulAdds означает одно, завтра — другое. Инкапсуляция не спасает, если логика внутри типа неаккуратная, но она хотя бы ограничивает радиус поражения.

Ошибка №5: Путать “публичный тип” и “публичную возможность создать что угодно”.
Очень частая ловушка: вы делаете public struct BookID, а потом автоматически делаете public init(rawValue:) и разрешаете создавать BookID(rawValue: ""). Потом вы добавляете валидацию и понимаете, что у вас уже есть внешний код, который создаёт «пустые» ID. Гораздо спокойнее сразу проектировать точки создания: оставить init не публичным и дать одну-две понятные фабрики. Это и есть инкапсуляция на уровне жизненного цикла объекта.

1
Задача
Swift SELF, 49 уровень, 4 лекция
Недоступна
Шагомер трекер
Шагомер трекер
1
Задача
Swift SELF, 49 уровень, 4 лекция
Недоступна
Коробка заметок
Коробка заметок
1
Задача
Swift SELF, 49 уровень, 4 лекция
Недоступна
Купоны каталога
Купоны каталога
1
Задача
Swift SELF, 49 уровень, 4 лекция
Недоступна
Фабрика ника
Фабрика ника
1
Опрос
SwiftPM Импорт, 49 уровень, 4 лекция
Недоступен
SwiftPM Импорт
Импорт, зависимости и уровни доступа
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ