JavaRush /Курсы /Swift SELF /Импорты и направления зависимостей

Импорты и направления зависимостей

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

1. Как связывать import и зависимости target’ов

import в Swift новичкам часто кажется чем-то вроде «магической мантры»: написал import Domain — и компилятор обязан понять и простить. К сожалению (или к счастью), компилятор — существо злопамятное и принципиальное. Он хочет, чтобы вы сначала объяснили сборочной системе, какие модули доступны данному target’у, и только потом подключали их в исходниках.

В SwiftPM import — это не просьба «пожалуйста, найди мне модуль где-нибудь во Вселенной». Это утверждение: «файл компилируется в окружении, где модуль X доступен». А вот доступен он или нет — решает граф зависимостей targets, который вы описали в Package.swift. В документации SwiftPM это формулируется очень прямолинейно: targets — базовые блоки пакета, и они могут зависеть от других targets и от products внешних зависимостей.

Модель «target → module → import»

Чтобы не путаться, зафиксируем простую «лесенку», на которой держится весь день:

  • Target — то, что SwiftPM компилирует отдельно.
  • Обычно каждому target соответствует module (модуль).
  • Если модуль доступен текущему target’у по зависимостям, вы можете написать import <ModuleName>.

Очень важно: вы импортируете не «папку» и не «файл», а именно модуль. Поэтому «лежит где-то рядом в Sources» ничего не гарантирует.

Представьте, что targets — это комнаты в офисе, а import — это дверь между ними. Package.swift решает, есть ли дверь вообще. А модификаторы доступа (public и друзья) решают, открыта ли дверь или на ней висит табличка «только для сотрудников отдела». Про доступы мы сегодня скажем ровно столько, сколько нужно, чтобы не словить ступор на пустом месте.

Небольшая схема:

flowchart LR
    A["Sources/Domain/..."] -->|компилируется| D["Target Domain<br/>Module: Domain"]
    B["Sources/Storage/..."] -->|компилируется| S["Target Storage<br/>Module: Storage"]
    C["Sources/LibraryCLI/..."] -->|компилируется| L["Target LibraryCLI<br/>Module: LibraryCLI"]

    L -->|"import Domain (если dependency есть)"| D
    S -->|"import Domain (если dependency есть)"| D

2. Граф зависимостей LibraryCLI: кто «выше», кто «ниже»

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

Для нашего LibraryCLI логика такая: домен в центре, инфраструктура вокруг домена, а CLI‑слой сверху — он всё собирает.

Граф (в идеале) выглядит так:

flowchart TD
    LibraryCLI["LibraryCLI (executable)"] --> Domain["Domain"]
    LibraryCLI --> Storage["Storage"]
    LibraryCLI --> Networking["Networking"]

    Storage --> Domain
    Networking --> Domain

Обратите внимание на отсутствие стрелок обратно. Если Domain начнёт зависеть от Storage, вы получите архитектурную «чёрную дыру»: домен станет знать про детали хранения, и через пару недель вы начнёте писать что-то вроде Domain/BookFileNameBuilder.swift (и да, это звучит так же грустно, как и выглядит).

4. Как зависимости target’ов разрешают или запрещают import

Теперь самое практичное: когда именно import разрешён.

Рассмотрим минимальный фрагмент Package.swift, где мы закрепляем зависимости между targets. Этот код короткий, но он определяет судьбу всех import в проекте.

import PackageDescription

let package = Package(
    name: "LibraryCLI",
    targets: [
        .executableTarget(
            name: "LibraryCLI",
            dependencies: ["Domain", "Storage", "Networking"]
        ),
        .target(name: "Domain"),
        .target(name: "Storage", dependencies: ["Domain"]),
        .target(name: "Networking", dependencies: ["Domain"]),
    ]
)

Теперь читаем это как правила импорта:

  • Внутри target LibraryCLI можно писать import Domain, import Storage, import Networking.
  • Внутри target Storage можно писать import Domain, но нельзя писать import LibraryCLI и нельзя писать import Networking (если мы это не указали).
  • Внутри target Domain нельзя писать import Storage, import Networking, import LibraryCLI, потому что у Domain нет таких зависимостей.

Именно отсюда берётся ошибка уровня «No such module …» — SwiftPM просто не даёт текущему target’у доступ к модулю.

5. Практика: что импортируем в каждом модуле

В этом разделе мы сделаем очень приземлённую вещь: посмотрим, как должны выглядеть файлы в каждом target’е, чтобы направления зависимостей были видны прямо в коде.

Domain: никаких знаний об инфраструктуре

В Domain мы держим модели и контракты. Например, простую модель книги:

// Sources/Domain/Book.swift
import Foundation

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

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

Здесь есть два момента, которые часто удивляют.

Первый момент: import Foundation не является SwiftPM‑зависимостью, это системный модуль, его можно импортировать «просто так» (если он нужен).

Второй момент: почему public? Потому что Domain — отдельный модуль. Если другой target (например, Storage) импортирует Domain, он увидит только public объявления. Всё, что без public, по умолчанию имеет уровень доступа internal и остаётся внутри модуля. Это не «ещё одна бюрократия», а нормальная модульная капсула.

Storage: импортируем Domain, реализуем контракт

Допустим, у нас есть репозиторий, который хранит книги в памяти (пока без файлов — это нормально для примера):

// Sources/Storage/InMemoryBookRepository.swift
import Foundation
import Domain

public struct InMemoryBookRepository {
    private var items: [Book] = []

    public init() {}

    public mutating func add(_ book: Book) {
        items.append(book)
    }
}

Здесь важно, что Storage зависит от Domain, значит import Domain легален. А обратная зависимость запрещена — и это как раз то, что сохраняет архитектуру в адекватном состоянии.

LibraryCLI: импортируем всё, но не превращаемся в «помойку логики»

В исполняемом target’е мы собираем всё вместе.

// Sources/LibraryCLI/main.swift
import Foundation
import Domain
import Storage

var repo = InMemoryBookRepository()
repo.add(Book(id: "1", title: "Swift для людей и котов"))

print("Books added: 1") // Books added: 1

CLI‑слой имеет право импортировать Storage, потому что это точка сборки. Но «иметь право» не означает «надо тащить сюда всю бизнес‑логику». В хорошем проекте LibraryCLI часто выглядит как место, где происходит парсинг команд, вызовы сервисов и печать результата — но не хранение правил домена.

6. Ошибки import: зачем они и как диагностировать

Почему ошибка import — это полезная защита

Когда вы впервые ловите ошибку вида:

  • No such module 'Domain'
  • или Cannot find type 'Book' in scope (при том, что тип точно где-то есть!)

возникает ощущение, что Swift «капризничает». На деле он делает ровно то, за что мы его ценим: не даёт проекту деградировать.

Если Domain вдруг смог бы импортировать Storage, вы бы очень быстро получили цикл:

  • Domain импортирует Storage (потому что «нужно прочитать файл»),
  • Storage импортирует Domain (потому что «нужно вернуть Book»),
  • и сборка встанет в тупик.

SwiftPM запрещает циклические зависимости targets, потому что невозможно собрать модуль A, если ему нужен модуль B, который, в свою очередь, требует модуль A ещё до того, как A существует. Это как пытаться надеть носок поверх ботинка, а ботинок — поверх носка, не снимая их с ноги.

Как понять, где реально проблема

Важный навык: по тексту ошибки быстро понять, чинить Package.swift или чинить исходники.

Если вы видите “No such module X”, то чаще всего проблема именно в том, что target, в котором лежит файл, не имеет зависимости на модуль X в Package.swift.

То есть мыслительный алгоритм такой: «В каком target лежит этот файл?» → «Есть ли у этого target зависимость на модуль, который я импортирую?» → «Совпадает ли имя модуля с именем target’а/product’а?»

Если же import проходит, но вы видите “Cannot find type … in scope”, то часто это уже история про уровни доступа: символ находится в другом модуле, но не public, поэтому снаружи его будто бы не существует.

Чтобы лучше запоминалось, маленькая таблица:

Симптом Частая причина Где чинить
No such module 'Domain'
target не зависит от
Domain
Package.swift
Cannot find 'Book' in scope
при
import Domain
Book
не
public
исходники
Domain
Ambiguous use of ...
после добавления зависимостей
конфликт имён / перегрузки чаще в исходниках, иногда в дизайне модулей

7. Внешние зависимости: .product(...) и конфликты модулей

Внешние пакеты: почему import работает только после .product(...)

До сих пор мы говорили про внутренние targets. Но в SwiftPM есть ещё одна важная ветка: внешние пакеты.

Там модель двухшаговая:

  1. вы объявляете пакет в dependencies пакета (то есть говорите «мы вообще используем этот внешний пакет»),
  2. вы добавляете конкретный product этого пакета в зависимости target’а (то есть говорите «вот этот модуль доступен вот этому target’у»),
  3. и только потом уже в исходниках появляется право писать import <ModuleName>.

В официальном гайде по SwiftPM это показано на примере подключения Figlet: target подключает product .product(name: "Figlet", package: "..."), после чего в коде можно сделать import Figlet.

Сильно упрощённый пример (без привязки к реальному пакету):

import PackageDescription

let package = Package(
    name: "LibraryCLI",
    dependencies: [
        .package(url: "https://example.com/cool-tools.git", from: "1.0.0")
    ],
    targets: [
        .executableTarget(
            name: "LibraryCLI",
            dependencies: [
                .product(name: "CoolTools", package: "cool-tools")
            ]
        )
    ]
)

И только тогда в Sources/LibraryCLI/... становится осмысленным:

import CoolTools

Если вы объявили пакет, но забыли .product(...) в target dependencies — import не сработает. Если вы добавили .product(...), но ошиблись в имени модуля — import тоже не сработает. (Да, это та самая ситуация, когда «я всё сделал, но оно не работает», и вы сидите с лицом человека, который нажал на лифт два раза, чтобы он приехал быстрее.)

Конфликт имён модулей и module aliasing

Иногда вы подключаете два разных пакета, а у них совпадают имена модулей. Например, оба экспортируют модуль Utils. В такой ситуации import Utils становится неоднозначным, и SwiftPM/компилятор не могут понять, что именно вы хотите.

Для таких случаев в SwiftPM есть механизм module aliasing: вы можете задать «переименование» модуля при подключении продукта, чтобы конфликт исчез. В proposal по module aliasing приводится пример, где одному из Utils задают алиас вроде GameUtils, чтобы в исходниках можно было явно импортировать нужный модуль.

Мы не будем углубляться (это редкий, но полезный инструмент), но важно знать: если вы внезапно упёрлись в конфликт модулей, проблема не в import как таковом, а в том, что в проекте стало два «одинаковых имени».

8. Типичные ошибки при импортах и зависимостях targets

Ошибка №1: писать import Domain, но не добавить Domain в зависимости target’а.
Это самая частая ситуация, и она коварна тем, что «вроде бы очевидно»: модуль же существует в проекте. Но для SwiftPM существование модуля в пакете не означает, что он доступен конкретному target’у. В результате вы ловите No such module 'Domain' и начинаете подозревать IDE в заговоре. Лечится это почти всегда проверкой Package.swift: у target’а, где лежит файл, должна быть зависимость на Domain.

Ошибка №2: перепутать имя пакета и имя модуля.
Внешний пакет может называться одним образом (identity/URL), а экспортируемый модуль (product/target name) — другим. Тогда вы честно прописали .package(url: ...), но пишете import не того имени и получаете «No such module». Хорошая привычка: в Package.swift смотреть на .product(name: ...) — вот это и есть имя модуля, которое вы будете импортировать, как в примере с Figlet.

Ошибка №3: забыть про public и думать, что import “должен открыть всё”.
import подключает модуль, но не отменяет инкапсуляцию. Если вы объявили struct Book без public в модуле Domain, то в модуле Storage этот тип будет невидим — и вы получите ошибку Cannot find type 'Book' in scope, хотя импорт формально работает. Это не баг и не «лишняя строгость Swift», а нормальные правила модульной видимости.

Ошибка №4: нарушить направление зависимостей (например, Domain импортирует Storage).
Иногда это начинается невинно: «я просто хочу прочитать файл прямо из Domain». Но это момент, когда архитектура начинает течь. Технически SwiftPM может даже не дать вам это сделать без явной зависимости, но если вы всё же добавите зависимость, дальше очень быстро появятся циклы, странные зависимости и ощущение, что проект «пахнет проводами». Если домену нужна операция сохранения, правильнее держать в домене контракт (протокол), а реализацию — в Storage, чтобы стрелка оставалась направленной к домену.

Ошибка №5: лечить No such module добавлением import Foundation «на всякий случай».
Иногда новички видят ошибку импорта и добавляют Foundation, потому что «так часто делают». Но Foundation тут ни при чём: проблема в графе зависимостей targets или в неправильном имени модуля. Foundation — системный модуль; он не чинит отсутствие модульной зависимости и не делает ваш import Domain внезапно правильным.

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