1. Полезно делить проект на targets
Если вы только начинаете программировать, очень легко поверить в магическую идею: «ну пусть всё будет в одном месте, потом разберусь». Это нормально — так делают почти все, включая людей, которые потом пишут статьи “How I refactored a ball of mud”. Проблема в том, что “потом” наступает неожиданно, а каша уже пригорела.
Когда проект становится больше пары файлов, появляются типичные боли: вы случайно тянете “хранение на диске” туда, где хотели чистую логику; сеть внезапно начинает торчать в доменных моделях; одна мелкая правка заставляет вас перепроверять половину приложения. Деление на targets решает это не «морально» (типа “будь дисциплинирован”), а технически: SwiftPM просто не даст импортировать то, что вы решили держать «ниже» по слоям.
Можно думать о targets как о комнатах в квартире. Если кухня (Storage) находится отдельно от спальни (Domain), вы физически не можете жарить котлеты в шкафу с одеждой. Теоретически можно очень стараться… но лучше не надо.
2. Что делим: 4 targets и роли
Перед тем как писать Package.swift, важно сначала договориться о смысле. Мы фиксируем в курсе единый нейминг и структуру: один исполняемый модуль и три “библиотечных” модуля. Причём ключевая деталь — исполняемый target и исполняемый продукт называются одинаково: LibraryCLI. Это уменьшает путаницу “что запускаем” vs “что импортируем”.
Ниже — таблица, которую полезно мысленно держать рядом, пока вы раскладываете файлы.
| Target | Что там лежит (идея) | Что НЕ кладём туда | От кого может зависеть |
|---|---|---|---|
|
Доменные модели, доменные ошибки, протоколы-контракты | Файлы, сеть, , чтение/запись |
Ни от кого внутри пакета |
|
Реализации хранения (в памяти/в файле позже), “репозитории” | CLI-парсинг, печать пользователю | Может зависеть от |
|
Сетевой слой, клиенты, DTO (позже), транспортные ошибки | CLI-парсинг и пользовательские сообщения | Может зависеть от |
|
Точка входа, сборка приложения (“склейка” модулей), обработка аргументов | Реализации хранения «внутри main.swift» | Может зависеть от всех трёх |
Обратите внимание на смысловую асимметрию: Domain — «низ» пирамиды. Он знает о бизнес‑понятиях, но ничего не знает о том, как мы читаем файлы или делаем HTTP.
3. Структура папок и связь с targets
Когда SwiftPM собирает пакет, он смотрит на Package.swift и связывает target с исходниками. Самая простая (и для новичков самая надёжная) схема: один target = одна папка внутри Sources/ с тем же именем.
В виде схемы это выглядит так:
LibraryCLI/
Package.swift
Sources/
LibraryCLI/
main.swift
Domain/
...
Storage/
...
Networking/
...
Важный практический момент: файлы внутри одного target компилируются вместе и образуют один модуль. Это та самая идея “targets — building blocks, defining a module”. И раз это модуль, значит потом (в следующей лекции дня) мы сможем говорить про import Domain, import Storage и т.д. Но сегодня нам важно другое: физическая граница в виде target’а уже задаёт “кто может знать о ком”.
4. Package.swift: targets и зависимости
Сейчас будет момент, где многие новички испытывают лёгкое чувство: “я ничего не понимаю, но оно выглядит важным”. Хорошие новости: вам не нужно запоминать весь синтаксис манифеста наизусть. Достаточно научиться его читать как «карту модулей»: какие targets существуют и кто от кого зависит.
Ниже — минимальный пример Package.swift для нашего разбиения на 4 targets:
// Package.swift
import PackageDescription
let package = Package(
name: "LibraryCLI",
products: [
.executable(name: "LibraryCLI", targets: ["LibraryCLI"])
],
targets: [
.executableTarget(
name: "LibraryCLI",
dependencies: ["Domain", "Storage", "Networking"]
),
.target(name: "Domain"),
.target(name: "Storage", dependencies: ["Domain"]),
.target(name: "Networking", dependencies: ["Domain"])
]
)
Здесь зашиты сразу несколько идей.
Во‑первых, LibraryCLI — это исполняемый target (точка входа), а ещё мы явно объявляем продукт .executable с тем же именем. Это как “файл .app” в мире CLI: то, что вы реально запускаете.
Во‑вторых, Storage и Networking зависят от Domain. Это означает: они могут использовать доменные типы и протоколы, но домен не знает о них ничего.
В‑третьих, Domain ни от кого не зависит. Если вам вдруг захотелось сделать Domain зависимым от Storage — это почти всегда признак, что вы перепутали слои местами (или пытаетесь решить проблему “в лоб”, и пора немного остановиться).
5. Граф зависимостей и отсутствие циклов
Когда вы смотрите на dependencies: [...] в Package.swift, полезно сразу переводить это в простую диаграмму. Так мозгу легче проверять корректность, чем глазами «ловить» строки.
Например, наш граф можно представить так:
flowchart TD
CLI[LibraryCLI<br/>executable] --> D[Domain]
CLI --> S[Storage]
CLI --> N[Networking]
S --> D
N --> D
Ключевое правило: граф зависимостей targets должен быть ацикличным. По‑человечески это означает: нельзя сделать “А зависит от B и B зависит от A”. Это тупик не только архитектурно, но и чисто технически: сборка не может “сначала собрать A, потому что нужен B, но сначала собрать B, потому что нужен A”.
6. Нейминг: что запускаем и что импортируем
Сейчас будет тонкий момент, который на практике экономит часы жизни.
Если вы назовёте исполняемый target App, а продукт оставите LibraryCLI, или наоборот — получится путаница:
- командой swift run ... вы запускаете продукт;
- в коде вы импортируете модули по имени targets.
И вы начнёте путать: “почему я запускаю LibraryCLI, а main.swift лежит в Sources/App?”, “почему я пишу import App, а проект называется LibraryCLI?”, “почему у меня в ошибке написано одно, а в папках другое?”.
Поэтому мы фиксируем правило курса: исполняемый target = LibraryCLI, и исполняемый продукт тоже = LibraryCLI. Это не “единственно правильное” в индустрии, но это очень правильное для обучения: меньше лишних переменных, больше понимания.
7. Мини-пример: код по модулям
Чтобы не было ощущения, что targets — это “просто красивые папки”, давайте сделаем маленький, но цельный пример. Мы не строим весь проект целиком (мы не устраиваем «домашку внутри лекции»), но показываем принцип: каждый слой содержит свой кусочек.
Domain: доменная модель и контракт
Начнём с самого “чистого” слоя — домена. Здесь мы заведём минимальную модель книги и протокол репозитория.
// Sources/Domain/Book.swift
public struct Book {
public let id: Int
public let title: String
public init(id: Int, title: String) {
self.id = id
self.title = title
}
}
Обратите внимание: здесь нет Foundation. Нам пока достаточно базовых типов Swift.
Теперь добавим контракт репозитория:
// Sources/Domain/BookRepository.swift
public protocol BookRepository {
func allBooks() -> [Book]
}
Почему протокол в домене? Потому что это “контракт”: домен говорит, что должно уметь хранилище, но не говорит, как именно оно будет это делать (в памяти, в файле, по сети — неважно).
Storage: реализация контракта из Domain
Теперь делаем слой, который “умеет хранить”. Для начала — супер‑простая in‑memory реализация (позже появятся файлы, JSON и всё такое, но не сегодня).
// Sources/Storage/InMemoryBookRepository.swift
import Domain
public struct InMemoryBookRepository: BookRepository {
public init() {}
public func allBooks() -> [Book] {
[Book(id: 1, title: "Swift для смелых"), Book(id: 2, title: "CLI без слёз")]
}
}
Здесь уже видно разделение ответственности: Storage импортирует Domain, потому что работает с доменными типами и протоколами. А вот Domain — не импортирует Storage. И это принципиально.
Networking: ещё одна реализация
Чтобы увидеть, что один и тот же контракт можно реализовать по‑разному, сделаем “сетевую” заглушку. Настоящую сеть мы пока не строим — просто имитируем, что “данные пришли”.
// Sources/Networking/FakeRemoteBookRepository.swift
import Domain
public struct FakeRemoteBookRepository: BookRepository {
public init() {}
public func allBooks() -> [Book] {
[Book(id: 100, title: "Книга из интернета (почти)")]
}
}
Да, это выглядит как Storage. И это нормально: мы сейчас тренируем границу, а не реализм. Реализм придёт позже, когда появятся реальные запросы, ошибки сети и DTO.
LibraryCLI: точка входа и “склейка” модулей
И наконец — исполняемый target. Здесь мы решаем, какую реализацию использовать, и выводим результат пользователю.
// Sources/LibraryCLI/main.swift
import Domain
import Storage
let repo = InMemoryBookRepository()
let books = repo.allBooks()
print("Books in library: \(books.count)") // Books in library: 2
Тут важно уловить стиль: LibraryCLI — это место, где разрешено “собирать зависимости” и решать конфигурацию. Если позже вы захотите выбрать FakeRemoteBookRepository вместо InMemoryBookRepository, это будет решение уровня композиции, а не домена.
8. Полезные нюансы
Targets и products: чем отличаются
Сейчас легко перепутать: targets, products, модули, “экспорт наружу”… Поэтому коротко, без попытки «объяснить всю вселенную».
Target — это единица компиляции, обычно модуль. Product — это то, что пакет “выдаёт наружу” (исполняемый артефакт или библиотека). В документах SwiftPM идея products и targets разделена именно для этого: у вас может быть много targets, но наружу вы отдаёте только часть в виде products.
При этом важно помнить (и это прямо спасает от неверных ожиданий): то, что вы создали target, ещё не означает “всё стало публичным API”. Видимость типов управляется модификаторами доступа (public/internal/private). Мы полноценно поговорим о доступах в свой день, а сегодня просто фиксируем: targets задают границу модулей, но не делают код автоматически публичным.
Признаки “здорового” разбиения
Есть простой тест, который можно держать в голове, не превращая обучение в религиозную войну за архитектуру.
- Если вы открываете Sources/Domain/... и видите там FileManager, URLSession, чтение аргументов командной строки или печать “Ошибка: ...” для пользователя — почти наверняка вы смешали слои. Домен должен быть максимально “скучным”: типы, протоколы, правила.
- Если вы открываете Sources/Storage/... и видите, что код начинает “парсить команды” или решать, какой exit code вернуть — это тоже тревожный сигнал. Storage — это инфраструктура хранения, а не мозг программы.
- Если вы открываете Sources/LibraryCLI/... и видите там огромные куски логики чтения/записи данных или сложные доменные проверки, это обычно значит, что LibraryCLI начал разрастаться как “божественный main.swift”. LibraryCLI должен быть дирижёром, а не оркестром.
9. Типичные ошибки
Ошибка №1: переименовали executable target и продукт в разные имена, и теперь всё путается.
Очень частая история: target назвали App, продукт — LibraryCLI, а потом вы не понимаете, что запускается командой swift run, где лежит точка входа и что именно вы импортируете в коде. На старте обучения лучше держать железное правило: исполняемый target и исполняемый продукт называются одинаково — LibraryCLI.
Ошибка №2: сделали Domain зависимым от Storage или Networking “потому что так проще”.
Это кажется удобным ровно до первого серьёзного изменения. Как только домен начинает знать про инфраструктуру, вы теряете смысл слоёв: у вас исчезает “чистое ядро”, и любое изменение хранения/сети начинает ломать бизнес‑логику. Правильная зависимость направлена в одну сторону: инфраструктура знает домен, домен не знает инфраструктуру.
Ошибка №3: положили все файлы в один target и сказали себе “потом разнесу”.
“Потом” обычно наступает после того, как уже страшно трогать проект. Targets особенно полезны именно рано: они дешёвые, пока проект маленький. Если вы сразу кладёте доменные типы в Domain, вам потом проще добавлять новые слои, не превращая main.swift в бесконечную простыню.
Ошибка №4: сделали внутренний target‑“свалку” и назвали его Utils, куда складываете вообще всё подряд.
Utils иногда действительно нужен, но для новичка это опасная “чёрная дыра”: туда начинают падать и доменные вещи, и форматирование строк, и чтение файлов, и “помощники для сети”. В результате вы получаете один огромный модуль, только с другим именем. Лучше держать targets “по ролям”: Domain, Storage, Networking, LibraryCLI.
Ошибка №5: пытаетесь решать проблемы видимости (public/private) только разбиением на targets.
Targets задают границы модулей, но они не заменяют модификаторы доступа. Можно случайно оставить типы “слишком доступными” внутри модуля или, наоборот, сделать что-то недоступным там, где нужно. Сегодня мы просто фиксируем границы слоёв, а “политику доступа” обсуждаем отдельно, когда придёт её время.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ