JavaRush /Курсы /Swift SELF /Разделение на targets: LibraryCLI, Domain, Storage

Разделение на targets: LibraryCLI, Domain, Storage

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

1. Полезно делить проект на targets

Если вы только начинаете программировать, очень легко поверить в магическую идею: «ну пусть всё будет в одном месте, потом разберусь». Это нормально — так делают почти все, включая людей, которые потом пишут статьи “How I refactored a ball of mud”. Проблема в том, что “потом” наступает неожиданно, а каша уже пригорела.

Когда проект становится больше пары файлов, появляются типичные боли: вы случайно тянете “хранение на диске” туда, где хотели чистую логику; сеть внезапно начинает торчать в доменных моделях; одна мелкая правка заставляет вас перепроверять половину приложения. Деление на targets решает это не «морально» (типа “будь дисциплинирован”), а технически: SwiftPM просто не даст импортировать то, что вы решили держать «ниже» по слоям.

Можно думать о targets как о комнатах в квартире. Если кухня (Storage) находится отдельно от спальни (Domain), вы физически не можете жарить котлеты в шкафу с одеждой. Теоретически можно очень стараться… но лучше не надо.

2. Что делим: 4 targets и роли

Перед тем как писать Package.swift, важно сначала договориться о смысле. Мы фиксируем в курсе единый нейминг и структуру: один исполняемый модуль и три “библиотечных” модуля. Причём ключевая деталь — исполняемый target и исполняемый продукт называются одинаково: LibraryCLI. Это уменьшает путаницу “что запускаем” vs “что импортируем”.

Ниже — таблица, которую полезно мысленно держать рядом, пока вы раскладываете файлы.

Target Что там лежит (идея) Что НЕ кладём туда От кого может зависеть
Domain
Доменные модели, доменные ошибки, протоколы-контракты Файлы, сеть,
URLSession
, чтение/запись
Ни от кого внутри пакета
Storage
Реализации хранения (в памяти/в файле позже), “репозитории” CLI-парсинг, печать пользователю Может зависеть от
Domain
Networking
Сетевой слой, клиенты, DTO (позже), транспортные ошибки CLI-парсинг и пользовательские сообщения Может зависеть от
Domain
LibraryCLI
Точка входа, сборка приложения (“склейка” модулей), обработка аргументов Реализации хранения «внутри 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 задают границы модулей, но они не заменяют модификаторы доступа. Можно случайно оставить типы “слишком доступными” внутри модуля или, наоборот, сделать что-то недоступным там, где нужно. Сегодня мы просто фиксируем границы слоёв, а “политику доступа” обсуждаем отдельно, когда придёт её время.

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