Паттерны проектирования, взгляд 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() {}
    
    }

    П.С.


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


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

    Поделиться публикацией

    Комментарии 33

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

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

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

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

              0
              Библиотеки взаимодействующие с некоторыми системными объектами необходимо реализовывать как синглтон. Обычно это какие-нибудь элементы управления системы: элементы управления плейером на «шторке» в iOS; «лоадер» в верхней инфо-полоске, keychain и т.п. Хотя можно разрешить юзеру создать два объекта, но смысла в этом нет, если они будут в итоге взаимосвязаны через системный объект. Нужно дать понять пользователю библиотеки что реально объект всего один, и не нужно пытаться с ним извращаться.
              • НЛО прилетело и опубликовало эту надпись здесь
                  +1
                  Не «библиотека-синглтонов», а «синглтоны полезны в системных библиотеках, чтобы дать пользователю библиотеки понять, что в системе этот объект единственный». Если разработчик не понимает, что объект всего один, то он может, например, попытаться взять «контакты», создать копию объекта и пытаться её использовать для отката состояния. Можно придумать ещё множество подобных примеров. Учтите, что есть люди, которые учатся программировать сразу под iOS, например. А есть более сложные примеры объектов, которые должны быть единственными. Естественно этот паттерн очень хитрый, и его нельзя использовать везде где хочется. Но когда ты пишешь библиотеку для разработчиков с неизвестным уровнем квалификации, то иногда нужно.
                  • НЛО прилетело и опубликовало эту надпись здесь
                +1
                «Должен отвечать DI» — значит отвечать придётся программисту, а смысл в том, чтобы по возможности не дать ему ошибиться. В конце концов, можно и синглтон инжектить если уж так хочется, чем он вам мешает?
                  +1
                  Вообще все антипатерны — это использование инструментов не по назначению. Вот допустим есть, топор для рубки дров, но им можно легко оттяпать свою же ногу -> топор стал антитопором. Берем отвертку хренакс она в глазу по ручку, хотя из начально разрабатывалась винтики крутить -> отвертка стала антиотверткой. Берем синглтон и сохраняемся в него из разных тредов -> синглетон стал антипатерном.

                  Ну и с другой стороны есть у Вас база данных, что вы на каждый запрос свою копию sql порождаете?
                  от сюда могу сделать вывод, что антипатерны появились благодаря кривым ручкам, которые не научились ими пользоваться. Это как тушить пожар бензином на космической станции наполненной чистым кислородом.
                  Я вот сам ООП до конца не понимаю, и считаю что ООП это сплошной антипатерн в FP.
                  Хотя есть друзья которые умеют Scala и я на них смотрю с белой завистью.
                    –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
                      Вот Вы только подтверждаете мои слова. Давайте ПО для спутников писать на Flash и потом статьи что Flash антипатер и мертвая технология? ;)
                        0

                        Попытаюсь перефразировать. Инженер как и любой другой человек подвержен заблуждениям и предрассудокам из-за которых сложно трезво оценивать технологии. Если мы всю жизнь использовали 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

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

                          p.s. за ссылки спасибо.
                        0
                        В каждой коммерческой программе сданной вовремя есть свой «GOTO», в том смысле что без технического долга невозможно писать экономически обоснованные крупные программы. Важно чтобы этого долга было мало. Но это уже вопрос уместности.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          +1
                          >Что в данном случае означает «копия SQL»?
                          ну я имею введу базу данных, на каждый запрос отдельно файл целый. Кстати идея очень хорошая. в Erlang гдето используют похожий подход.

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

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

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

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

                            Ну и да, антипаттерн — это не про тот или иной конкретный паттерн, а про неправельное его применение.
                              +1
                              О этот комент, надо распечатать и в кабинете повесить!
                              +1
                              private(set) empty = true
                              private(set) boiled = false
                              
                              // И getter'ы убрать
                              

                              По'swift'ее будет вроде как.
                                –1
                                По'swift'ее будет и так:

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

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


                                  // Swift
                                  class Singleton {
                                  
                                      static let shared = Singleton()
                                  
                                      private init() {}
                                  
                                  }
                                    0
                                    Спасибо за
                                    private init()
                                    , забыл «маленькую» деталь.

                                    А почему не вставить в тело поста короткий код? Почему только в конце?
                                      0

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

                                  +1
                                  Если не требуется изменять или проверять property на что-то, то в Objective-C можно писать вот-так:
                                  @property (assign, nonatomic, getter=isEmpty) BOOL empty;
                                  @property (assign, nonatomic, getter=isBoiled) BOOL boiled;

                                  и тогда не нужно реализовывать гетеры
                                    0

                                    Да, так удобнее и правильнее.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое