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


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


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


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


Содержание:


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


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


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


Основная задача синглтона — предоставить пользователю один и только один объект определенного класса на весь жизненный цикл приложения. В 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() {}

}

П.С.


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


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

Поделиться с друзьями
-->

Комментарии (29)


  1. Virasio
    02.02.2017 18:53
    +1

    Вот мой вариант синглтона на Objective-C, который использует по минимуму имя класса: https://gist.github.com/virasio/9941612 И ещё я запрещаю alloc и new.


    1. s_suhanov
      02.02.2017 22:44

      Да, так определенно лучше. Кстати насчет alloc: если запретить alloc, то запрещать init еще есть смысл или можно тогда этого не делать?


      1. Virasio
        03.02.2017 00:00
        +1

        Вы заставили меня задуматься. :) Похоже, что реально в таком случае init уже не нужно запрещать. Но нужно будет обдумать этот вопрос ещё раз на свежую голову.


  1. Mendel
    02.02.2017 19:52

    Обоих языков не знаю, так что больно не бейте. Но возник вопрос — а как на счет клонирования?


  1. Sirikid
    02.02.2017 21:28
    +2

    А что насчет того что это вообще антипеттерн и за конкретное кол-во объектов должен отвечать DI?


    1. s_suhanov
      02.02.2017 21:52

      Я думаю, что нужно это вопрос переадресовать Apple насчет UIApplication и NSUserDefaults. :)


    1. Virasio
      02.02.2017 22:20

      Библиотеки взаимодействующие с некоторыми системными объектами необходимо реализовывать как синглтон. Обычно это какие-нибудь элементы управления системы: элементы управления плейером на «шторке» в iOS; «лоадер» в верхней инфо-полоске, keychain и т.п. Хотя можно разрешить юзеру создать два объекта, но смысла в этом нет, если они будут в итоге взаимосвязаны через системный объект. Нужно дать понять пользователю библиотеки что реально объект всего один, и не нужно пытаться с ним извращаться.


      1. Sirikid
        02.02.2017 22:39
        -1

        Не совсем понял насчет библиотек-синглтонов (что это?) и извращений. Что плохого если пользователь не будет знать что на самом деле объект всего один?
        s_suhanov легаси есть легаси, плохой код от того что его нельзя выкинуть не становится хорошим.


        1. Virasio
          02.02.2017 23:57
          +1

          Не «библиотека-синглтонов», а «синглтоны полезны в системных библиотеках, чтобы дать пользователю библиотеки понять, что в системе этот объект единственный». Если разработчик не понимает, что объект всего один, то он может, например, попытаться взять «контакты», создать копию объекта и пытаться её использовать для отката состояния. Можно придумать ещё множество подобных примеров. Учтите, что есть люди, которые учатся программировать сразу под iOS, например. А есть более сложные примеры объектов, которые должны быть единственными. Естественно этот паттерн очень хитрый, и его нельзя использовать везде где хочется. Но когда ты пишешь библиотеку для разработчиков с неизвестным уровнем квалификации, то иногда нужно.


          1. Sirikid
            03.02.2017 01:56

            Библиотеки взаимодействующие с некоторыми системными объектами необходимо реализовывать как синглтон.

            Не понял вот этого пассажа.


            Если разработчик не понимает, что объект всего один, то он может, например, попытаться взять «контакты», создать копию объекта и пытаться её использовать для отката состояния.

            Разумно, но это уже другая проблема, если объект нельзя копировать значит у него не должно быть такого метода.


    1. DnV
      02.02.2017 22:36
      +1

      «Должен отвечать DI» — значит отвечать придётся программисту, а смысл в том, чтобы по возможности не дать ему ошибиться. В конце концов, можно и синглтон инжектить если уж так хочется, чем он вам мешает?


    1. Bimawa
      03.02.2017 09:27
      +1

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

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


      1. shai_xylyd
        03.02.2017 11:03
        -3

        Да ладно, идите объясните Дейкстре, что goto это нормально [1] и он просто не умеет его готовить, потом расскажите сэру Tony Hoare что null ссылки это ок [2], ну а под конец поведайте Erich Gamma и Ralph Johnson, что они заблуждаются называя синглтон своей ошибкой [3][4].


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


        [1] http://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf
        [2] https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare
        [3] http://www.informit.com/articles/article.aspx?p=1404056
        [4] https://twitter.com/daverooneyca/status/529799186795884544


        1. Bimawa
          03.02.2017 11:15
          +1

          Вот Вы только подтверждаете мои слова. Давайте ПО для спутников писать на Flash и потом статьи что Flash антипатер и мертвая технология? ;)


          1. shai_xylyd
            04.02.2017 05:35

            Попытаюсь перефразировать. Инженер как и любой другой человек подвержен заблуждениям и предрассудокам из-за которых сложно трезво оценивать технологии. Если мы всю жизнь использовали goto, то нам сложно признать что это было ошибкой. Такой эффект известен в психологии как эскалация обязательств [1].


            Вопрос в том, как замечать такое поведение и избегать его. Хороший подход — смотреть за авторами технологий и гигантами индустрии. Если изобртатель технологии смог перешагнуть через свою гордость и признать ошибку, то вероятно что-то дейсвительно с ней не так. Ссылки выше были как раз про это. Tony Hoare придумал null ссылки, Erich Gamma и Ralph Johnson — синглтоны.


            Я согласен, что каждому инструменту — свое назначение, просто назначение некоторых инструментов — отправиться на свалку истории :)


            Забавно что вы упоминули спутники, вот тут [2] пишут что там как раз используют наработки сэра Tony Hoare и наисвежайшие идеи, такие как TLA+. В общем, не держитесь за то, что уже доказало свою несостоятельность.


            [1] https://en.wikipedia.org/wiki/Escalation_of_commitment
            [2] http://www.altreonic.com/content/main-mission-rosettas-philae-lander-accomplished


            1. Bimawa
              04.02.2017 07:33

              Дак да, я полностью с этим согласен, я пытаюсь донести мысль о том, что синглетон меня ни разу не подвел при создании ТОДО приложения ;) И я хорошо понимаю какое зло синглетон при подсчетах бухгалтерии в многопоточности.

              p.s. за ссылки спасибо.


        1. Mendel
          03.02.2017 11:37

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


      1. Sirikid
        03.02.2017 11:10

        Ну и с другой стороны есть у Вас база данных,

        Классический черный ящик.


        что вы на каждый запрос свою копию sql порождаете?

        Что в данном случае означает "копия SQL"?


        Я вот сам ООП до конца не понимаю, и считаю что ООП это сплошной антипатерн в FP.
        Хотя есть друзья которые умеют Scala и я на них смотрю с белой завистью.

        Scala один из самых мощных функциональных языков современности.


        Оффтоп

        Вот Objective-C причудливая смесь C и Smalltalk поэтому и код на нем такой, хотя некоторый шарм у него есть.


        1. Bimawa
          03.02.2017 11:18
          +1

          >Что в данном случае означает «копия SQL»?
          ну я имею введу базу данных, на каждый запрос отдельно файл целый. Кстати идея очень хорошая. в Erlang гдето используют похожий подход.

          >Scala один из самых мощных функциональных языков современности.
          Которая умеет ООП ;)


    1. Mendel
      03.02.2017 11:29
      +1

      А DI у вас как реализован? Даже если у нас зависимости вызываются из прикладного объекта напрямую, то есть общее место где оно хранится, и это или статика, или синглтон (который тоже под собой подразумевает статику, но не суть).
      Статику сложнее тестировать, поэтому синглтон в этом плане лучше.
      Поэтому СервисЛокатор лучше таки делать синглтоном.
      Если приложение небольшое, у нас буквально один или два класса-одиночки. Городить DI особо смысла нет. Ну или нужна пачка одиночек в рамках отдельной подкомпоненты, и не хочется их держать в общем стеке. Тут тоже можно или пачку одиночек, или отдельный сервисЛокатор. По ситуации.
      Так то я соглашусь что в реальном проекте оно нужно не часто, но это же классика.
      У меня лично синглтон один — СервисЛокатор).


  1. Psionic
    03.02.2017 00:15

    Ой, ну что тут сказать — синглатон на обжектив-си, как правило далеко не синглотон, особенности языка таковы что если очень захотеть — можно создать другой объект, разве что alloc/init выбросить из класса после первого вызова. Так что с недоступным инитом могли не заморачиваться.


    1. s_suhanov
      03.02.2017 12:26

      В первом комментарии уточнили уже насчет alloc-а. :)


      1. Virasio
        03.02.2017 15:16
        +2

        s_suhanov, нет, тут Psionic немного о другом говорит, как я понимаю. Objective-C в рантейме позволяет что только не творить, и подобный запрет не гарантирует полной недоступности методов. И при этом можно просто удалить из объекта класса (как я понимаю) методы alloc и init (и initPrivate), после того как первый объект был создан. Вот быстренько нашел немного про рантайм: https://habrahabr.ru/post/177421/
        Кстати, ещё есть allocWithZone, например, который и я не запретил.
        Для Psionic хочу заметить только то, что паттерны используются не для того чтобы что-то уж совсем так перекрыть со всех сторон, а просто для того, чтобы другой разработчик понимал что же тут было задумано. Если ему хочется головной боли, то пусть лезет в рантайм, но описание класса как синглтона должно дать понять разработчику-пользователю библиотеки, что автор библиотеки не предполагал создания нескольких объектов, и не стоит потом ему предъявлять претензии. Образно говоря: автор библиотеки пистолет поставил на предохранитель, но пользователь может снять с предохранителя и всё же отстрелить себе мешающую ногу. :)


  1. kostyl
    03.02.2017 09:43
    +1

    Что опять?


  1. DjoNIK
    03.02.2017 10:34
    +4

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

    Плохая идея. Паттерны, это не про реализацию набора правил, это про применение этого набора правил в подходящем для этого месте. Хотя, может и нужно пройти такого рода эволюцию:
    1) паттерн головного мозга — это когда только прочитал про них и везде, чаще не к месту, применяешь их;
    2) просветление — это когда реально познал дзен и можешь аргументированно рассказать почему в этом месте нужен именно этот паттерн, а при немного других вводных при той же функциональности уже нет.

    Ну и да, антипаттерн — это не про тот или иной конкретный паттерн, а про неправельное его применение.


    1. Bimawa
      04.02.2017 07:39
      +1

      О этот комент, надо распечатать и в кабинете повесить!


  1. risabd
    06.02.2017 07:30
    +1

    private(set) empty = true
    private(set) boiled = false
    
    // И getter'ы убрать
    

    По'swift'ее будет вроде как.


  1. briahas
    10.02.2017 17:42
    -1

    По'swift'ее будет и так:

    class MyClass: {
        static let shared = MyClass()
    }
    


    1. s_suhanov
      10.02.2017 17:44

      На всякий случай повторю концовку поста. :)


      // Swift
      class Singleton {
      
          static let shared = Singleton()
      
          private init() {}
      
      }