JavaRush /Курсы /Swift SELF /API‑дизайн Swift — labels, нейминг

API‑дизайн Swift — labels, нейминг

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

1. Имена функций в Swift: base name и labels

Если вы только начинаете, кажется, что главное — чтобы программа работала: добавили книгу, удалили книгу, вывели список — ура. Но в проекте чуть побольше выясняется, что вы гораздо чаще читаете код, чем пишете. А API — это именно то, что вы читаете на call site: в месте вызова.

Представьте, что ваш модуль Domain — это кофемашина. Можно сделать 17 кнопок без подписей и инструкцию на 40 страниц. А можно сделать три большие кнопки: “Espresso”, “Americano”, “Milk”. В обоих случаях кофе получится. Но во втором случае шанс, что пользователь случайно сварит “латте без чашки”, заметно ниже. В коде роль подписей выполняют имена функций и labels.

В языках вроде Java/C# многие привыкли: «имя метода — это слово до скобок». В Swift всё хитрее: argument labels являются частью имени. Исторически это поддерживает две важные вещи: читаемость вызова и возможность иметь несколько перегрузок с одинаковым base name, но разными labels (например, как у UIKit‑подобных API). Эта идея явно проговаривается в материалах Swift Evolution про compound names и различение перегрузок по меткам аргументов.

Labels — часть имени функции

То есть, грубо говоря, функция:


func move(from: Int, to: Int)

имеет имя (в терминах Swift) move(from:to:), а не просто move.

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

Call site как предложение

Когда говорят «читается как предложение», обычно имеют в виду: если вы смотрите на строку кода, вам не надо лезть в реализацию, чтобы понять смысл параметров. Swift прямо поощряет этот стиль: многие решения языка и стандартной библиотеки исторически приводили к тому, что labels стали “первым классом” читаемости.

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

Сравним две версии одного и того же действия в нашем CLI‑проекте LibraryCLI.

Пример: плохой call site без labels

struct Library {
    mutating func addBook(_ book: Book) {
        // ...
    }
}

Вызов будет таким:

library.addBook(book)

Вроде нормально. Но проблема появляется, когда параметров становится два-три, или когда типы одинаковые (например, String, String). Тогда без labels легко перепутать.

Пример: лучше — смысл читается из вызова

struct Library {
    mutating func add(_ book: Book) {
        // ...
    }

    mutating func removeBook(withID id: BookID) {
        // ...
    }
}

Вызовы:

library.add(book)
library.removeBook(withID: id)

Здесь “withID” — это маленькая подсказка мозгу: «сейчас будет ID, а не название, не индекс и не “что-то ещё”».

Когда скрывать label через _

Очень хочется сделать API «короче» и начать везде писать _, убирая внешние метки. Но в Swift _ — это не «делаем красиво», а «мы уверены, что смысл аргумента очевиден без подписи».

Хорошее правило для новичка: если вы сомневаетесь, нужен ли label — скорее всего, нужен. Swift в целом двигается в сторону того, чтобы labels помогали читателю, а не мешали ему (это видно и по подходу к переводу Objective‑C API в Swift‑стиле, где активно добавляются/нормализуются labels и выкидываются лишние слова).

Пример: _ уместен, когда аргумент «главный»

struct BookID {
    let rawValue: String
}

func findBook(_ id: BookID) {
    print("Searching \(id.rawValue)")
    // Searching 123
}

Здесь _ можно оправдать: в контексте findBook(...) аргумент почти всегда «и так понятно что это ID».

Пример: labels спасают, когда параметры легко перепутать

func move(from startIndex: Int, to endIndex: Int) {
    print("move from \(startIndex) to \(endIndex)")
    // move from 2 to 10
}

move(from: 2, to: 10)

Если убрать labels, вы получите классическое «а что было первым: откуда или куда?». А такие ошибки особенно неприятны тем, что код компилируется и “даже работает”… но делает не то.

Предлоги в labels: from, to, in, at, with, for

Swift любит, когда labels выглядят как предлоги и образуют фразу. Более того, язык даже специально решал проблему, что некоторые хорошие предлоги совпадают с ключевыми словами (например, in). В Swift Evolution отдельно обсуждалось, что такие keywords стоит разрешить как argument labels, потому что они действительно улучшают выразительность API.

Это означает, что “человеческий” API может выглядеть так:

indexOf(value, in: collection)

И это не «странность», а буквально то, чего Swift добивается — чтобы интерфейс читался как предложение.

Для нашего LibraryCLI это переводится в практику:

  • searchBooks(containing:) — “ищи книги, содержащие строку”
  • removeBook(withID:) — “удали книгу с таким ID”
  • updateBook(withID:to:) — “обнови книгу с ID на новое значение”

Смысл labels здесь — показать роль аргумента, а не его тип.

3. Нейминг по эффекту и по смыслу

Когда вы проектируете API, вы не только называете функцию — вы даёте обещание: “эта операция делает вот это”. Если назвать метод process() или handle(), вы обещаете… ничего. Компилятор не против, но пользователи вашего кода будут страдать молча (а потом громко).

Глаголы для действий и «не обещай лишнего»

Очень практичный приём: различать операции по эффекту.

Если метод меняет состояние, имя обычно глагольное: add, remove, update. Если метод возвращает вычисленное значение, имя часто выглядит как существительное/прилагательное: count, isEmpty, title.

В стандартной библиотеке и вокруг неё много примеров, где нейминг подталкивает к правильному чтению и правильному ожиданию. Исторически Swift также старался «вырезать лишние слова, повторяющие тип», чтобы оставался смысл, а не тавтология.

Пример: мутирующая операция в Library

struct Book {
    let id: String
    let title: String
}

struct Library {
    private(set) var books: [Book] = []

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

Обратите внимание: add — коротко и глагольно. addBook тоже можно, но если контекст уже “Library”, слово Book иногда становится лишним (мы ещё вернёмся к “omit needless words”).

Булевы свойства и методы: is…, has…, can…

Булевы значения — особая боль новичка: true/false сами по себе ничего не объясняют, всё решает имя.

Swift‑стиль обычно такой: булевы свойства читаются как утверждения о получателе: isEmpty, isValid, hasPrefix, canUndo. Даже при переводе Objective‑C API в Swift отдельно подчёркивалось, что булевым свойствам логично добавлять is, чтобы они читались как утверждение.

Пример: плохой bool, который не читается

struct LibraryCLIConfig {
    let verbose: Bool
}

func verbose(_ config: LibraryCLIConfig) -> Bool {
    config.verbose
}

Вызов if verbose(config) { ... } выглядит странно: “если verbose(config)… что?”

Пример: лучше — читается как фраза

struct LibraryCLIConfig {
    let isVerbose: Bool
}

func isVerbose(_ config: LibraryCLIConfig) -> Bool {
    config.isVerbose
}

Теперь if isVerbose(config) { ... } читается почти как английское предложение: “if is verbose”.

Omit needless words: убираем слова, которые повторяют контекст

Одна из самых частых ошибок — «назвать всё максимально подробно»: addBookToLibrary(book: Book). Кажется, что так понятнее. Но на практике это превращается в шум.

Swift‑подход часто такой: если контекст уже понятен из типа/места, слово можно убрать. В документах про перевод Objective‑C имён в Swift показано, как из API убираются куски, которые повторяют тип, и как результат становится ближе к “читаемости по смыслу”, а не по тавтологии.

Сравним на примерах, близких к нашему LibraryCLI.

Плохо (шумно) Лучше (смысл) Почему
addBookToLibrary(book:)
add(_:)
или
add(book:)
“Library” уже в типе получателя
findBookByID(id:)
book(withID:)
“ByID” — лишнее, withID уже говорит роль
removeBookFromLibrary(id:)
removeBook(withID:)
“FromLibrary” лишнее, мы и так в Library

Важно: “omit needless words” не означает “делаем всё коротким”. Оно означает “убираем то, что не добавляет смысла”.

4. Перегрузки и ссылки на методы: роль labels

Swift позволяет иметь несколько методов с одинаковым base name, которые различаются labels. Это удобно, но требует дисциплины: labels должны действительно отражать разные смыслы, а не быть «синонимами ради синонимов».

Перегрузки с одним base name

В обсуждении generalized naming приводится классическая ситуация: один base name (insertSubview), три разных действия, и именно labels делают API однозначным.

Перенесём идею в Library.

Пример: несколько способов поиска, различаем по labels

struct Library {
    private(set) var books: [Book] = []

    func book(withID id: String) -> Book? {
        books.first { $0.id == id }
    }

    func books(containing titlePart: String) -> [Book] {
        books.filter { $0.title.contains(titlePart) }
    }
}

Вызовы будут выглядеть так:

let one = library.book(withID: "42")
let many = library.books(containing: "Swift")

Одно и то же слово book/books, но разные “формы”: один объект vs много объектов, и это видно прямо из сигнатуры.

Когда метод передаётся как значение

Эта часть кажется «теорией», пока вы не начнёте активно использовать замыкания и функции как значения. В Swift бывает так, что несколько методов имеют один base name, и без указания labels ссылка на метод становится неоднозначной.

В Swift Evolution по generalized naming это описано прямо: если есть три insertSubview, то someView.insertSubview — неоднозначно, и Swift ввёл/обсуждал синтаксис, позволяющий ссылаться на метод вместе с labels, вроде insertSubview(_:at:).

Для нашего курса это полезно как мысль: labels — это не “косметика”, а часть имени, которая иногда реально влияет на то, сможете ли вы удобно использовать API в более функциональном стиле.

5. Мини‑рефакторинг LibraryCLI: делаем API «разговорным»

Сейчас соберём несколько маленьких кусочков в одну линию. Мы не строим новый модуль и не уходим в архитектуру; наша цель — показать, как нейминг меняет ощущение от кода, хотя логика почти та же.

Шаг 1: доменные типы с понятными именами

struct BookID: Hashable {
    let rawValue: String
}

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

BookID говорит сам за себя. rawValue — нормальная конвенция для “обёрток” вокруг примитивов.

Шаг 2: Library с API, который читается в вызове

struct Library {
    private(set) var books: [Book] = []

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

    func book(withID id: BookID) -> Book? {
        books.first { $0.id == id }
    }
}

Вызовы выглядят естественно:

library.add(book)
let found = library.book(withID: id)

Обратите внимание: мы используем withID, а не byID или id:. Это не “единственно правильное” слово, но оно хорошо работает как часть фразы.

Шаг 3: команды CLI тоже выигрывают от нейминга

enum Command {
    case add(title: String)
    case find(id: String)
}

Да, это не labels в вызове функции, но принцип тот же: add(title:) читается лучше, чем add(String) — особенно когда у команды несколько параметров.

6. Типичные ошибки в API‑дизайне Swift

Ошибка №1: “Сделаю покороче и уберу все labels через _”.
Поначалу кажется, что без labels код компактнее. Но как только появляется два параметра одного типа, вы начинаете путать аргументы местами. Самое неприятное, что компилятор часто не может вас спасти: String и String выглядят одинаково. Хороший стиль Swift как раз ценит labels, и исторически язык двигался к унификации поведения меток для всех параметров, включая первый.

Ошибка №2: “Назову метод максимально подробно: addBookToLibrary”.
Такое имя создаёт иллюзию понятности, но на деле превращает вызов в тавтологию: library.addBookToLibrary(book). Swift‑подход часто обратный: убирать слова, которые повторяют информацию, уже известную из контекста типа. Такой стиль особенно виден на примерах «очистки» имён при переводе Objective‑C API в Swift‑манеру.

Ошибка №3: булевы значения названы так, что true/false непонятны.
Если у вас flag, mode, enabled, ok — это красный флаг (простите за каламбур). Вызов if config.verbose { ... } ещё терпим, но когда булево значение спрятано за функцией verbose(config) — читателю приходится расшифровывать, что именно значит true. В Swift часто используют is/has/can, и даже при автоматическом переводе API в Swift отмечалась полезность префикса is для булевых свойств.

Ошибка №4: labels подобраны как «синонимы», а не как разные смыслы.
Если вы делаете removeBook(by:), removeBook(with:), removeBook(using:) — это не выразительность, это путаница. Перегрузки по labels в Swift действительно существуют и полезны, но они должны отражать разные смысловые операции, как в классическом примере с одним base name и несколькими вариантами поведения, различаемыми по labels.

Ошибка №5: “универсальная функция” с названием handle/process/doStuff.
Это самая частая причина, почему проект через месяц становится нечитаемым: вы открываете файл и видите service.handle(...), manager.process(...), controller.do(...). Такие имена не закрепляют контракт. Гораздо полезнее потратить ещё 30 секунд и назвать действие по смыслу: add, remove, search, parse, format. Да, это скучно. Но скучный код — часто самый надёжный (и это тот вид скуки, за который платят зарплату).

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