Почему VIPER это плохой выбор для вашего следующего приложения

Этот пост является вольным переводом статьи Why VIPER is a bad choice for your next application by Sergey Petrov

За последний год о VIPER писали все кому не лень. Эта архитектура реально вдохновляет разработчиков. Но большинство статей, на самом деле, довольно предвзяты. Они лишь показывают крутизну этого архитектурного паттерна, умалчивая о его негативных сторонах. А ведь проблем у него вовсе не меньше (а может даже и больше) чем у других. И в этой статье я постараюсь объяснить, почему VIPER вовсе не так хорош как о нем говорят, и почему он не подойдет для большинства ваших приложений.

Некоторые статьи о сравнении архитектур, как правило, утверждают что VIPER совершенно не похож на другие MVC-архитектуры. Но на самом деле, VIPER — это просто нормальный MVC, где контроллер разделен на две части: interactor и presenter. View остался на месте, а модель переименована в entity. Router заслуживает особого внимания: да, другие архитектуры не упоминают эту часть в своих аббревиатурах, но он присутствует и в них: в неявном виде (когда вы вызываете pushViewController — вы создаете простой маршрутизатор) или более очевидном (как пример — FlowCoordinators).

Поговорим о «плюшках», которые предлагает нам VIPER (я буду ссылаться на эту книгу). Посмотрим на цель номер два, в которой говорится о SRP (принцип единой ответственности). Это прозвучит грубо, но каким чудаком нужно быть, чтобы считать это преимуществом? Вам платят за решение задач, а не за соответствие модным словам. Да, вы до сих пор используете TDD, BDD, юнит-тестирование, Realm или SQLite, внедрение зависимостей и много-много других вещей, но вы используете все это не просто ради использования, а для решения проблем клиента.

Тестирование.

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

Одна из главных причин в том, что нет хороших примеров. Вы можете найти довольно много статей на тему того как написать юнит-тест assert 2 + 2 == 4, но реальных примеров не найдете (тем не менее Artsy держит в опен-сорсе свои приложения, и вам следует взглянуть на их проекты).

VIPER предлагает отделить всю логику во множество мелких классов с разделенными обязанностями. Это должно бы облегчить тестирование, но так не всегда. Да, написать юнит-тест для простого класса легко, но большинство из таких тестов ничего не тестируют. Давайте посмотрим, например, на большинство методов презентера: они лишь прокси между вью и другими компонентами. Вы можете написать тесты для этого прокси, это увеличит тестовое покрытие вашего кода, но эти тесты бесполезны. И у вас будет побочный эффект: вы должны обновлять эти бесполезные тесты после каждого внесения изменений в код.

Правильный подход к тестированию должен бы включать в себя тестирование интерактора и презентера сразу, ведь эти две части сильно связаны друг с другом. Кроме того, поскольку мы разделяем логику на два класса, нам нужно намного больше тестов по сравнению с одним классом. Это простая комбинаторика: у класса A есть 4 возможных состояния, а у класса B — 6, соответственно их комбинация имеет 24 возможных состояния, и вам нужно их протестировать.

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

Как ни странно, тестировать вью проще чем тестировать некоторые участки бизнес-логики. Вью представляет собой лишь совокупность определенных свойств и наследует внешний вид из этих свойств. Вы можете использовать FBSnapshotTestCase для сравнения их состояния с внешним видом. Эта штука все еще не обрабатывает некоторые особые случаи, как кастомные переходы, но насколько часто вы их используете?

Оверинжиниринг в проектировании.

VIPER — то, что происходит, когда бывшие джависты врываются в мир iOS. — n0damage, комментарий на reddit

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

Представьте себе простую задачу: есть кнопка, которая запускает обновление с сервера и есть вью, с полученными от сервера данными. Угадайте-ка сколько классов/протоколов будут задеты таким изменением? Да, как минимум 3 класса и 4 протокола будут изменены для реализации такой простой функции. Кто-нибудь помнит как Spring начали с некоторых абстракций и закончили с AbstractSingletonProxyFactoryBean? Я всегда мечтал об «удобном суперклассе прокси-фабрики для прокси-фабрик, которые создают только синглтоны» в моем коде.

Избыточные компоненты


(Избыточный код не так безобиден, как я привык думать. Он дает ложный сигнал о своей необходимости.)

Как я уже упоминал ранее, презентер, как правило, довольно глупый класс, который просто передает вызовы из вью в интерактор (что-то вроде этого). Да, иногда он содержит сложную логику, но в основном, это просто избыточный компонент.

«DI-френдли» количество протоколов


(Некрасивый код легко распознать и его стоимость легко оценить. Это не так если абстракция неверна.)

Существует общая путаница с этим сокращением: VIPER реализует принципы SOLID, где DI — это «инверсия зависимостей», а не «внедрение» («dependency inversion«, а не «injection«). Внедрение зависимостей — это особый случай паттерна «инверсия управления» («Inversion of Control»), который, конечно связан, но отличается от инверсии зависимостей.

Инверсия зависимостей — это про отделение модулей разных уровней путем ввода абстракций между ними. Для примера, модуль UI не должен напрямую зависеть от сетевого модуля. Инверсия управления — это другое. Это когда модуль (обычно из библиотеки, которую мы не можем менять) делегирует что-нибудь другому модулю, который обычно предоставлен первому модулю как зависимость. Да, когда вы реализуете data source для вашей UITableView вы используете принцип IoC. Использование схожих названий для различных высокоуровневых вещей — источник путаницы.

Вернемся к VIPER. Есть много протоколов (как минимум 5) между классами внутри одного модуля. И во всех них нет необходимости. Презентер и интерактор не являются модулями из разных слоев. Применение принципа IoC может иметь смысл, но спросите себя: как часто у вас бывает хотя бы два презентера для одного вью? Я уверен, что большинство из вас ответит «никогда». Так почему же необходимо создавать эту кучу протоколов, которые мы никогда не будем использовать?

Кроме того, из-за этих протоколов, вы не можете легко перемещаться по коду в IDE. Ведь cmd+click будет вас выбрасывать в протокол, вместо реализации.

Проблемы с производительностью

Это ключевой момент, но многие просто не переживают об этом, или просто недооценивают влияние плохих архитектурных решений.

Я не буду говорить о фреймворке Typhoon (который очень популярен для внедрения зависимостей в мире objective-c). Конечно, он имеет некоторое влияние на производительность, особенно когда используется автоматическое внедрение, но VIPER не требует его использования. Вместо этого, я бы поговорил о рантайнме и запуске приложения, и как VIPER замедляет ваше приложение буквально повсюду.

Время запуска приложения. Эта тема редко обсуждается, но она важна. Ведь если ваше приложение стартует очень медленно, то пользователи не будут им пользоваться. На последней WWDC как раз говорили об оптимизации времени запуска приложений. Время старта вашего приложения напрямую зависит от количества классов в нем. Если у вас есть 100 классов — это нормально, задержка будет незаметной. Однако, если в вашем приложении только 100 классов — вам действительно нужна эта сложная архитектура? Но если ваше приложение огромно, например, вы работаете над приложением Facebook (18К классов), то разница будет ощутима: около одной секунды. Да, «холодный старт» вашего приложения займет 1 секунду только на то, чтоб подгрузить все метаданные классов и больше ничего, вы правильно поняли.

Рантайм вызовы. Тут все сложнее и в основном применяется только для Swift-компилятора (поскольку рантайм Objective-C располагает бОльшими возможностями и компилятор не может безопасно выполнять оптимизации). Давайте поговорим о том, что происходит «под капотом» когда вы вызываете какой-нибудь метод (я говорю «вызываете», а не «отправляете сообщение» потому, что второе не всегда корректно для Swift). Есть три вида вызовов в Swift (от быстрого к медленному): статический, таблица вызовов и отправка сообщений. Последний — единственный, который используется в Objective-C, и он используется в Swift когда необходима совместимость с кодом Objective-C, или когда метод объявлен как dynamic. Конечно, эта часть рантайма будет весьма оптимизирована и записана в ассемблере для всех платформ. Но что если мы сможем избежать этих накладных расходов, дав компилятору понятие о том, что именно будет вызываться, во время компиляции? Это именно то, что Swift-компилятор делает со статикой и таблицей вызовов. Статические вызовы быстры, но компилятор не может их использовать без 100% уверенности в типах. И когда тип нашей переменной является протоколом, компилятор вынужден использовать вызовы с помощью таблиц. Это не слишком медленно, но одна миллисекунда здесь, одна — там, и теперь общее время выполнения вырастает более чем на одну секунду, по сравнению с тем, чего можно было бы добиться с чистым Swift-кодом. Этот пункт связан с предыдущим о протоколах, но я думаю, что лучше отделить беспокойство по поводу количества неиспользуемых протоколов от возни с компилятором.

Слабое разделение абстракций

Должен быть один и, желательно, только один очевидный способ сделать это.

Один из самых популярных вопросов VIPER-сообщества: «куда мне следует отнести X?» Получается, что с одной стороны есть много правил, как нужно делать все правильно, а с другой — многие решения основаны на чьем-нибудь мнении. Это могут быть сложные случаи, например обработка CoreData с помощью NSFetchedResultsController, или UIWebView. Но даже общие случаи, такие как использование UIAlertController — являются темой для обсуждения. Давайте взглянем: здесь с нашим алертом взаимодействует роутер, а здесь его показывает вью. Вы можете ответить, что этот простой алерт является частным случаем алерта без каких-либо действий, кроме закрытия.

Особые случаи недостаточно особы, чтобы нарушать правила.

Все верно, но зачем у нас здесь фабрика для создания таких алертов? В итоге имеем бардак даже с UIAlertController. Вы этого хотите?

Генерация кода

Читаемость имеет значение.

Как это может быть архитектурной проблемой? Это просто генерация кучи классов по шаблону вместо написания их вручную. В чем здесь проблема? Проблема в том, что большинство времени вы читаете код, а не пишете его. Таким образом вы читаете шаблонный код вперемешку со своим кодом большую часть своего времени. Хорошо ли это? Не думаю.

Заключение.

Я не преследую цель отговорить вас от использования VIPER вообще. Вполне могут быть приложения, которые от него только выигрывают. Однако, прежде чем начать разработку своего приложения вам следует задать себе пару вопросов:

  1. Будет ли у этого приложения длительный жизненный цикл?

  2. Достаточно ли стабильны требования? В противном случае вы столкнетесь с бесконечным рефакторингом даже при внесении мелких изменений.

  3. Вы действительно тестируете свои приложения? Будьте честны с собой.

Только если вы ответили «да» на все три вопроса, VIPER мог бы быть хорошим выбором для вашего приложения.

И, наконец, последнее: вы должны самостоятельно принимать решения. Не просто слепо доверять какому-то парню с Медиума (или Хабра), который говорит «Используйте X, X — это круто.» Этот парень тоже может ошибаться.

Реклама

Паттерны проектирования, взгляд iOS разработчика. Часть 1. Стратегия

Содержание:

Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Часть 2. Наблюдатель

Напомню, что в этой серии статей, я разбираю книгу «Паттерны проектирования» Эрика и Элизабет Фримен. И сегодня мы изучим паттерн «Стратегия». Поехали.

Откуда растут ноги (и крылья)

Авторы книги рассказывают нам историю о создании приложения SimUDuck. Начнем с реализации начального состояния приложения: у нас есть абстрактный класс Duck и два его наследника: MallardDuck и RedheadDuck. Тут же мы сталкиваемся с первой сложностью: в Objective-C и Swift нет абстрактных классов.

Выходим из ситуации теми инструментами, что есть: для Objective-C объявляем обычный класс Duck (в нем будут реализации по умолчанию) и к нему добавляем протокол AbstractDuck (в нем будут абстрактные методы, которые нужно реализовать в наследниках). Выглядит это так:

// Objective-C
@protocol AbstractDuck <NSObject>

- (void)display;

@end

@interface Duck : NSObject

- (void)quack;
- (void)swim;

@end

Соответственно наследники будут такими:

// Objective-C
@interface MallardDuck : Duck <AbstractDuck>

@end

@implementation MallardDuck

- (void)display {

}

@end

@interface RedheadDuck : Duck <AbstractDuck>

@end

@implementation RedheadDuck

- (void)display {

}

@end

В Swift это сделать немного проще: достаточно протокола и его расширения (в расширении можно некоторые методы протокола реализовать по умолчанию):

// Swift
protocol Duck {
    func quack()
    func swim()
    func display()
}

extension Duck {

    func quack() {

    }

    func swim() {

    }

}

И наследники:

// Swift
class MallardDuck: Duck {

    func display() {

    }

}

class RedheadDuck: Duck {

    func display() {

    }

}

Приложение развивается и у уток появляется возможность летать

Для этого соответствующий метод появляется в родительском классе Duck. И вскоре после этого выясняется, что есть еще один наследник — RubberDuck. Резиновые утки ведь не летают, а поскольку метод добавлен в родительский класс, то он будет доступен и для резиновых уток. В общем: ситуация оказалась не из простых. При дальнейшем расширении приложения будут возникать сложности с поддержкой функций полета (и не только с ней, с функцией кряканья та же история) и с другими видами уток (деревянных, например).

Сначала авторы книги предлагают решать проблему вынесением функций полета и кряканья в отдельные интерфейсы (для Objective-c и Swift — протоколы) Flyable и Quackable. Но этот вариант оказывается совсем не так хорош, каким кажется на первый взгляд. Малейшее изменение функции полета, которое должно быть применено ко всем летающим уткам влечет за собой внесение одного и того же кода во многих местах программы. Так что такое решение определенно не подходит.

(говоря о негодности этого варианта, авторы ссылаются на то, что в интерфейсах (для нас протоколах) нет реализаций по умолчанию, но это справедливо лишь для Objective-C, а вот в Swift реализации по умолчанию для полета и кряканья можно было бы написать в расширениях этих протоколов и переопределять эти функции только там где необходимо, а не везде)

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

Для этого создадим протоколы FlyBehavior и QuackBehavior:

// Objective-C
@protocol FlyBehavior <NSObject>

- (void)fly;

@end

@protocol QuackBehavior <NSObject>

- (void)quack;

@end
// Swift
protocol FlyBehavior {
    func fly()
}

protocol QuackBehavior {
    func quack()
}

И конкретные классы реализующие эти протоколы: FlyWithWings и FlyNoWay для FlyBehavior, а также Quack, Squeak и MuteQuack для QuackBehavior (приведу пример для FlyWithWings, остальные реализуются очень схожим образом) :

// Objective-C
@interface FlyWithWings : NSObject <FlyBehavior>

@end

@implementation FlyWithWings

- (void)fly {

    // fly implementation

}

@end
// Swift
class FlyWithWings: FlyBehavior {

    func fly() {

        // fly implementation

    }

}

Делегирование наше все

Теперь мы, по сути, делегируем наше поведение любому другому классу, который реализует соответствующий интерфейс (протокол). Как сказать нашей утке каким должно быть ее поведение в полете и при кряканьи? Очень просто, добавляем в наш класс (в Swift — протокол) Duck два свойства:

// Objective-C
@property (strong, nonatomic) id<FlyBehavior> flyBehavior;
@property (strong, nonatomic) id<QuackBehavior> quackBehavior;
// Swift
var flyBehavior: FlyBehavior { get set }
var quackBehavior: QuackBehavior { get set }

Как видите у них не определен конкретный тип, определено лишь, что это класс подписанный на соответствующий протокол.

Методы fly и quack нашего родительского класса (или протокола) Duck заменим аналогичными:

// Objective-C
- (void)performFly {
    [self.flyBehavior fly];
}

- (void)performQuack {
    [self.quackBehavior quack];
}
// Swift
func performFly() {
    flyBehavior.fly()
}

func performQuack() {
    quackBehavior.quack()
}

Теперь наша утка просто делегирует свое поведение соответствующему поведенческому объекту. Как мы устанавливаем поведение каждой утке? Например при инициализации (пример для MallardDuck):

// Objective-C
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.flyBehavior = [[FlyWithWings alloc] init];
        self.quackBehavior = [[Quack alloc] init];
    }
    return self;
}
// Swift
init() {
    self.flyBehavior = FlyWithWings()
    self.quackBehavior = Quack()
}

Наш паттерн готов 🙂

Заключение

В iOS разработке паттерн «Стратегия» вы можете встретить, например, в архитектуре MVP: в ней презентер является не чем иным как поведенческим объектом для вью-контроллера (вью-контроллер, как вы помните, только сообщает презентеру о действиях пользователя, а вот логику обработки данных определяет презентер), и наоборот: вью-контроллер — поведенческий объект для презентера (презентер лишь говорит «показать пользователю данные», но как именно они будут показаны — решит вью-контроллер). Также этот паттерн вы встретите и в VIPER, если, конечно, надумаете его использовать в вашем приложении. 🙂

Паттерны проектирования, взгляд iOS разработчика. Часть 0. Синглтон-Одиночка

Я почув і забув.
Я записав і запам’ятав.
Я зробив і зрозумів.
Я навчив іншого, тепер я майстер.
(В. В. Бублик)

Небольшое вступление.

Я не зря вынес в начало поста цитату на украинском языке. Дело в том, что именно эти слова я услышал от своего преподавателя программирования на втором курсе университета, и именно в таком виде я вспоминаю эти слова до сих пор. Как вы можете догадаться, эта цитата является отсылкой к высказыванию Конфуция, но в ней есть очень важное дополнение о достижении мастерства.

И именно эти слова и сподвигли меня на написание данной серии постов. Дело в том, что я — начинающий iOS разработчик, и я очень хочу разобраться в паттернах проектирования. И я не придумал лучшего способа, чем взять книгу «Паттерны проектирования» Эрика и Элизабет Фримен, и написать примеры каждого паттерна на Objective-C и Swift. Таким образом я смогу лучше понять суть каждого паттерна, а также особенности обоих языков.

Содержание:

Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Часть 2. Наблюдатель

Итак, начнем с самого простого на мой взгляд паттерна.

Одиночка, он же — синглтон.

Основная задача синглтона — предоставить пользователю один и только один объект определенного класса на весь жизненный цикл приложения. В iOS-разработке, как по мне — самый лучший пример необходимости такого объекта — класс UIApplication. Вполне логично, что в течение жизни нашего приложения у нас должен быть один-единственный объект класса UIApplication.

Итак, разберемся что такое синглтон в Objective-C и Swift на примерах из книги.

Давайте сначала узнаем как вообще создать объект какого-нибудь класса. Очень просто:

// Objective-C
[[MyClass alloc] init]
// Swift
MyClass()

И тут авторы подводят нас к мысли, что приведенным выше способом можно создать сколько угодно объектов этого класса. Таким образом, первое что нужно сделать на пути к синглтону — запретить создание объектов нашего класса извне. В этом нам поможет приватный инициализатор.

И если в swift это реализуется тривиально:

// Swift
class MyClass {

    private init() {}

}

То в objective-c все не так просто на первый взгляд. Дело в том, что все классы obj-c имеют одного общего предка: NSObject, в котором есть общедоступный инициализатор. Поэтому в файле заголовка нашего класса нужно указать на недоступность этого метода для нашего класса:

// Objective-C
@interface MyClass : NSObject

- (instancetype)init UNAVAILABLE_ATTRIBUTE;

@end

Таким образом попытка создать объект нашего класса извне вызовет ошибку на этапе компиляции. Окей. Теперь и в objective-c у нас есть запрет на создание объектов нашего класса. Правда это еще не совсем приватный инициализатор, но мы к этому вернемся через пару секунд.

Итак, по сути мы получили класс, объекты которого не могут создаваться, потому что конструктор — приватный. И что со всем этим делать? Будем создавать объект нашего класса внутри нашего же класса. И будем использовать для этого статический метод (метод класса, а не объекта):

// Swift
class MyClass {

    private init() {}

    static func shared() -> MyClass {
        return MyClass()
    }

}
// Objective-C
@implementation MyClass

+ (instancetype)sharedInstance {
    return [[MyClass alloc] init];
}

@end

И если для swift опять все просто и понятно, то с objective-c возникает проблема с инициализацией:

Вполне логично, ведь мы сказали ранее, что - (instancetype)init недоступен. И он недоступен в том числе и внутри нашего класса. Что делать? Написать свой приватный инициализатор в файле реализации и использовать его в статическом методе:

// Objective-C
@implementation MyClass

- (instancetype)initPrivate
{
    self = [super init];
    return self;
}

+ (instancetype)sharedInstance {
    return [[MyClass alloc] initPrivate];
}

@end

(да, и не забудьте вынести метод + (instancetype)sharedInstance в файл заголовка, он должен быть публичным)

Теперь все компилируется и мы можем получать объекты нашего класса таким способом:

// Objective-C
[MyClass sharedInstance]
// Swift
MyClass.shared()

Наш синглтон почти готов. Осталось только исправить статический метод так, чтобы объект создавался только один раз:

// Objective-C
@implementation Singleton

- (instancetype)initPrivate
{
    self = [super init];
    return self;
}

+ (instancetype)sharedInstance {
    static Singleton *uniqueInstance = nil;
    if (nil == uniqueInstance) {
        uniqueInstance = [[Singleton alloc] initPrivate];
    }
    return uniqueInstance;
}

@end
// Swift
class Singleton {

    private static var uniqueInstance: Singleton?

    private init() {}

    static func shared() -> Singleton {
        if uniqueInstance == nil {
            uniqueInstance = Singleton()
        }
        return uniqueInstance!
    }

}

Как видите, для этого нам понадобилась статическая переменная, в которой и будет храниться единожды созданный объект нашего класса. Каждый раз при вызове нашего статического метода она проверяется на nil и, если объект уже создан и записан в эту переменную — он не создается заново. Наш синглтон готов, ура! 🙂

Теперь немного примеров из жизни из книги.

Итак, у нас есть шоколадная фабрика и для приготовления мы используем высокотехнологичный нагреватель шоколада с молоком (я просто обожаю молочный шоколад), который будет управляться нашим программным кодом:

// Objective-C

// файл заголовка ChocolateBoiler.h
@interface ChocolateBoiler : NSObject

- (void)fill;
- (void)drain;
- (void)boil;
- (BOOL)isEmpty;
- (BOOL)isBoiled;

@end

// файл реализации ChocolateBoiler.m
@interface ChocolateBoiler ()

@property (assign, nonatomic) BOOL empty;
@property (assign, nonatomic) BOOL boiled;

@end

@implementation ChocolateBoiler

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.empty = YES;
        self.boiled = NO;
    }
    return self;
}

- (void)fill {
    if ([self isEmpty]) {
        // fill boiler with milk and chocolate
        self.empty = NO;
        self.boiled = NO;
    }
}

- (void)drain {
    if (![self isEmpty] && [self isBoiled]) {
        // drain out boiled milk and chocolate
        self.empty = YES;
    }
}

- (void)boil {
    if (![self isEmpty] && ![self isBoiled]) {
        // boil milk and chocolate
        self.boiled = YES;
    }
}

- (BOOL)isEmpty {
    return self.empty;
}

- (BOOL)isBoiled {
    return self.boiled;
}

@end
// Swift
class ChocolateBoiler {

    private var empty: Bool
    private var boiled: Bool

    init() {
        self.empty = true
        self.boiled = false
    }

    func fill() {
        if isEmpty() {
            // fill boiler with milk and chocolate
            self.empty = false
            self.boiled = false
        }
    }

    func drain() {
        if !isEmpty() && isBoiled() {
            // drain out boiled milk and chocolate
            self.empty = true
        }
    }

    func boil() {
        if !isEmpty() && !isBoiled() {
            // boil milk and chocolate
            self.boiled = true
        }
    }

    func isEmpty() -> Bool {
        return empty
    }

    func isBoiled() -> Bool {
        return boiled
    }

}

Как видите — нагреватель сначала заполняется смесью (fill), затем доводит ее до кипения (boil), и после — передает ее на изготовление молочных шоколадок (drain). Для избежания проблем нам нужно быть уверенными, что в нашей программе присутствует только один экземпляр нашего класса, который управляет нашим нагревателем, поэтому внесем изменения в программный код:

// Objective-C
@implementation ChocolateBoiler

- (instancetype)initPrivate
{
    self = [super init];
    if (self) {
        self.empty = YES;
        self.boiled = NO;
    }
    return self;
}

+ (instancetype)sharedInstance {
    static ChocolateBoiler *uniqueInstance = nil;

    if (nil == uniqueInstance) {
        uniqueInstance = [[ChocolateBoiler alloc] initPrivate];
    }

    return uniqueInstance;
}

// other methods

@end
// Swift
class ChocolateBoiler {

    private var empty: Bool
    private var boiled: Bool

    private static var uniqueInstance: ChocolateBoiler?

    private init() {
        self.empty = true
        self.boiled = false
    }

    static func shared() -> ChocolateBoiler {
        if uniqueInstance == nil {
            uniqueInstance = ChocolateBoiler()
        }
        return uniqueInstance!
    }

    // other methods

}

Итак, все отлично. Мы на 100% уверены (точно на 100%?), что у нас есть только один объект нашего класса и никаких непредвиденных ситуаций на фабрике не произойдет. И если наш код на objective-c выглядит довольно неплохо, то swift выглядит недостаточно swifty. Попробуем его немного переписать:

// Swift
class ChocolateBoiler {

    private var empty: Bool
    private var boiled: Bool

    static let shared = ChocolateBoiler()

    private init() {
        self.empty = true
        self.boiled = false
    }

    // other methods

}

Дело в том, что мы можем спокойно хранить наш объект-одиночку в статической константе shared и нам совсем необязательно писать для этого целый метод с проверками на nil. Сам объект будет создан при первом обращении к этой константе и записан в нее один-единственный раз.

А как же многопоточность?

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

И опять же: в swift, как оказывается, совершенно не нужно выполнять каких-либо дополнительных действий. Константа уже потокобезопасна, ведь значение в нее может быть записано только один раз и это сделает тот поток, который доберется до нее первым.

А вот в objective-c необходимо внести коррективы в наш статический метод:

// Objective-C
+ (instancetype)sharedInstance {
    static ChocolateBoiler *uniqueInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        uniqueInstance = [[ChocolateBoiler alloc] initPrivate];
    });

    return uniqueInstance;
}

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

Итоги подведем.

Итак, мы разобрались как правильно писать синглтоны на objective-c и swift. Приведу вам итоговый код класса Singleton на обоих языках:

// Objective-C

// файл заголовка Singleton.h
@interface Singleton : NSObject

- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)sharedInstance;

@end

// файл реализации Singleton.m
@implementation Singleton

- (instancetype)initPrivate
{
    self = [super init];
    return self;
}

+ (instancetype)sharedInstance {
    static Singleton *uniqueInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        uniqueInstance = [[Singleton alloc] initPrivate];
    });

    return uniqueInstance;
}

@end
// Swift
class Singleton {

    static let shared = Singleton()

    private init() {}

}

П.С.

Хочу попросить всех читателей и комментаторов: если вы увидели какую-нибудь неточность или знаете как какой-либо из приведенных мною кусков кода написать правильнее/красивее/корректнее — скажите об этом. Я пишу здесь совсем не для того, чтоб учить других, а для того, чтоб научиться самому. Ведь пока мы учимся — мы остаемся молодыми.

Спасибо вам за внимание.