Синглтон (Перевод с английского главы «Singleton» из книги «Pro Objective-C Design Patterns for iOS» Carlo Chung)

в 16:47, , рубрики: design patterns, iOS, ipad, iphone, objective-c, singleton, разработка под iOS, метки: , , , , ,

В математике и логике синглтон определяется как «множество, содержащее ровно один элемент». Поэтому неважно, насколько велика сумка, каждый раз при попытке достать из неё шарик будем получать один и тот же. В каких ситуациях нужен синглтон в программировании? Подумайте о ресурсах, которые невозможно скопировать, но можно использовать совместно. Например, на iPhone установлен единственный модуль GPS и определять текущие координаты умеет только он. Класс CLLocationManager из фреймворка CoreLocation предоставляет единственную точку входа ко всем сервисам GPS-модуля. Кто-нибудь может подумать: если можно сделать копию CLLocationManager, можно ли получить дополнительный набор GPS-сервисов для своего приложения? Это звучит, как фантастика – вы создали два программных GPS по цене одного аппаратного. Но в реальности вы все равно получаете только один GPS единовременно, так как в iPhone есть только один GPS, который создает настоящие соединения со спутниками. Так что, если вы думаете, что создали супер-приложение, которое может манипулировать двумя отдельными GPS-соединениями одновременно, и хотите похвастаться этим перед друзьями, подумайте дважды.

Класс синглтона в объектно-ориентированном приложении всегда возвращает один и тот же экземпляр самого себя. Он обеспечивает глобальную точку доступа для ресурсов, которые предоставляет объект класса. Паттерн с такой функциональностью называется Синглтон.
В этой главе мы изучим возможности реализации и использования паттерна Синглтон в Objective-C и фреймворке Cocoa Touch на iOS.

Что из себя представляет паттерн Синглтон?

Паттерн Синглтон – едва ли не самый простой из паттернов. Его назначение в том, чтобы сделать объект класса единственным экземпляром в системе. В первую очередь нужно запретить создавать более одного экземпляра класса. Для этого можно использовать фабричный метод (глава 4), который должен быть статическим, так как не имеет смысла разрешать экземпляру класса создавать другой единственный экземпляр. Рисунок 7-1 показывает структуру класса простого синглтона.

image
Рисунок 7-1. Статическая структура паттерна Синглтон.

Статический uniqueInstance – это единственный экземпляр класса Singleton, представленный в виде переменной класса, которую статический метод sharedInstance вернет клиентам. Обычно sharedInstance будет проверять, инстанцирован ли uniqueInstance. Если нет, метод создаст его перед возвратом.

Примечание. Паттерн Синглтон: Проверяет, что есть только один экземпляр класса и обеспечивает единую точку доступа к нему.*
*Исходное определение, представленное в «Design Patterns» GoF (Addison-Wesley,
1994).

Когда можно использовать паттерн Синглтон?

Есть смысл подумать об использовании паттерна Синглтон, если:

  • В системе может быть только один экземпляр класса, который должен быть доступен через хорошо известную точку доступа, например, фабричный метод.
  • Единственный экземпляр может быть расширен только путем наследования, и клиентский код не потеряет работоспособность от использования расширенного объекта.

Паттерн Синглтон обеспечивает привычный способ создания уникального экземпляра с удобным способом доступа к разделяемому ресурсу. Метод использования ссылки на статический глобальный объект не предотвращает создание другого экземпляра того же класса. Подход использования метода класса, хотя и может предоставить глобальную точку доступа, но ему недостает гибкости разделения кода.
Статическая глобальная переменная содержит единственную ссылку на экземпляр класса. Другой класс или метод, которые имеют к ней доступ, фактически разделяют ту же самую копию с другими классами или методами, использующими ту же переменную. Похоже на то, что нам нужно. Все выглядит замечательно, пока используется та же самая глобальная переменная по всему приложению. Таким образом, фактически, паттерн Синглтон не нужен. Но подождите! Что, если кто-то в вашей команде определил в коде такую же глобальную переменную, как ваша? Тогда будет две копии одного и того же глобального объекта в одном приложении – поэтому глобальная переменная на самом деле не решает проблему.

Метод класса предоставляет возможность разделения без создания его объекта. Единственный экземпляр ресурса поддерживается в методе класса. Однако этот подход имеет недостаток гибкости в случае, если класс требует наследования для обеспечения большей функциональности.

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

Существует также гибкая версия паттерна Синглтон, в которой фабричный метод всегда возвращает один и тот же экземпляр, но можно в дополнение аллоцировать и инициализировать другие экземпляры. Эта менее строгая версия паттерна обсуждается в разделе «Использование класса NSFileManager» далее в этой главе.

Реализация Синглтона в Objective-C

Есть кое-что, над чем стоит подумать, чтобы спроектировать класс Синглтона правильно. Первый вопрос, которым нужно задаться, — это как удостовериться, что только один экземпляр класса может быть создан? Клиенты в приложении, написанном на других объектно-ориентированных языках, таких, как C++ и Java, не могут создать объект класса, если его конструктор объявлен закрытым. А как обстоят дела в Objective-C?

Любой Objective-C метод является открытым, и язык сам по себе динамически типизированный, поэтому любой класс может послать сообщение другому (вызов метода в C++ и Java) без значительных проверок во время компиляции (только предупреждения компилятора, если метод не объявлен). Также фреймворк Cocoa (включая Cocoa Touch) использует управление памятью методом подсчета ссылок для поддержания времени жизни объекта в памяти. Все эти особенности делают реализацию Синглтона в Objective-C довольно непростой.
В оригинальном примере книги «Паттерны проектирования» пример Синглтона на C++ выглядел, как показано в листинге 7-1.

Листинг 7-1. Исходный пример на C++ паттерна Синглтон из книги «Паттерны проектирования».

class Singleton
{
public:
    static Singleton *Instance();
protected:
    Singleton();
private:
    static Singleton *_instance;
};

Singleton *Singleton::_instance = 0;

Singleton *Singleton::Instance()
{
    if (_instance == 0)
    {
        _instance = new Singleton;
    }
    return _instance;
}

Как описано в книге, реализация в C++ проста и прямолинейна. В статическом методе Instance() статическая переменная _instance проверяется на 0 (NULL). Если так, то создается новый объект класса Singleton и затем возвращается. Кто-то из вас может подумать, что Objective-C версия не сильно отличается от своего собрата и должна выглядеть, как в листингах 7-2 и 7-3.

Листинг 7–2. Объявление класса Singleton в Singleton.h

@interface Singleton : NSObject
{
}

+ (Singleton *) sharedInstance;

@end

Листинг 7–3. Реализация метода sharedInstance Singleton.m

@implementation Singleton

static Singleton *sharedSingleton_ = nil;

+ (Singleton *) sharedInstance
{
    if (sharedSingleton_ == nil)
    {
        sharedSingleton_ = [[Singleton alloc] init];
    }
    return sharedSingleton_;
}
@end

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

  • Вызывающий объект не может создать объект Синглтона через другие средства аллокации. Иначе станет возможным создание множества экземпляров класса Синглтона.
  • Ограничения на создание объекта Синглтон должны быть согласованы с моделью подсчета ссылок.

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

Листинг 7–4. Более подходящая реализация Singleton в Objective-C

#import "Singleton.h"

@implementation Singleton

static Singleton * sharedSingleton_ = nil;

+ (Singleton*) sharedInstance
{
    if (sharedSingleton_ == nil)
    {
        sharedSingleton_ = [[super allocWithZone:NULL] init];
    }
    return sharedSingleton_;
}

Внутри метода sharedInstance, так же, как и в первом примере, сначала проверяется, что единственный экземпляр класса создан, иначе создается новый и возвращается. Но в этот раз он вызывает [[super allocWithZone:NULL] init] для создания нового экземпляра вместо использования других методов, таких, как alloc. Почему super, а не self? Это потому, что базовые методы выделения памяти под объект в нашем классе переопределены, поэтому нужно «одолжить» эту функциональность у базового класса, в данном случае у NSObject, чтобы помочь сделать низкоуровневую рутинную работу по выделению памяти для нас.

+ (id) allocWithZone:(NSZone *)zone
{
    return [[self sharedInstance] retain];
}

- (id) copyWithZone:(NSZone*)zone
{
    return self;
}

Есть несколько методов, которые относятся к управлению памятью в классе Singleton, о которых нужно подумать. В методе allocWithZone:(NSZone *)zone происходит просто возврат экземпляра класса, который возвращается из метода sharedInstance. Во фрейворке Cocoa Touch при вызове метода класса allocWithZone:(NSZone *)zone память под экземпляр будет выделена, его счетчик ссылок будет установлен в 1, и экземпляр будет возвращен. Мы видели, что метод alloc используется во многих ситуациях; фактически alloc вызывает allocWithZone: с зоной, установленной в NULL, чтобы выделить память под экземпляр в зоне по умолчанию. Детали создания объекта и управления памятью находятся за пределами данной книги. Вы можете обратиться к документации для дальнейших разъяснений.

Аналогично нужно переопределить метод copyWithZone:(NSZone*)zone, чтобы убедиться, что он не вернет копию экземпляра, а вернет тот же самый, возвращая self.

- (id) retain
{
    return self;
}
- (NSUInteger) retainCount
{
    return NSUIntegerMax; // показывает, что объект не может быть освобожден
}
- (void) release
{
    // ничего не делает
}
- (id) autorelease
{
    return self;
}
@end

Другие методы, такие, как retain, release и autorelease, переопределяются, чтобы убедиться, что они не сделают ничего (в модели управления памятью с подсчетом ссылок), кроме как вернут self. Метод retainCount возвращает NSUIntegerMax (4,294,967,295) для предотвращения удаления экземпляра из памяти в течение жизни приложения.

Зачем вызывать retain Синглтону?
Возможно, вы заметили, что мы вызываем retain объекту Синглтона, который возвращается из метода sharedInstance в allocWithZone:, но retain переопределяется и фактически игнорируется в нашей реализации. Учитывая это, у нас будет возможность сделать класс Синглтона менее «строгим» (то есть, можно будет выделять память под дополнительные экземпляры и инициализировать их, но фабричный метод sharedInstance всегда возвращает один и тот же экземпляр или объект Синглтона становится разрушаемым). Подклассы могут переопределять методы retain, release и autorelease снова, чтобы обеспечить подходящую реализацию управления памятью.
Гибкая версия паттерна Синглтон обсуждается в разделе «Использование класса NSFileManager» далее в этой главе.

Мы уже достаточно подробно рассмотрели, как должен выглядеть паттерн Синглтон в Objective-C. Однако есть еще кое-что, о чем стоит хорошенько подумать, прежде чем можно будет его использовать. Что, если мы хотим наследовать от первоначального класса Singleton? Рассмотрим, как это сделать.

Наследование от Синглтона

Вызов alloc перенаправляется в super, поэтому класс NSObject позаботится о выделении памяти под объект. Если мы наследуемся от класса Singleton без модификаций, то возвращаемый экземпляр всегда будет типа Singleton. Поскольку класс Singleton переопределяет все относящиеся к созданию экземпляра методы, довольно непросто наследовать от него. Но нам повезло; можно использовать некоторые функции Foundation для инстанцирования любого объекта, основываясь на его типе класса. Одна из них – это id NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone). Поэтому, если мы хотим инстанцировать объект класса, называющийся «Singleton», можно сделать следующее:

Singleton *singleton = [NSAllocateObject ([Singleton class], 0, NULL) init];

Первый параметр – это тип класса Singleton. Второй параметр предназначен для любого количества дополнительных байт для индексированных переменных экземпляра, который всегда равен 0. Третий параметр – обозначение зоны выделяемой памяти; почти всегда используется зона по умолчанию (параметр равен NULL). Поэтому можно инстанцировать любые объекты, используя эту функцию, зная тип класса. Что нужно делать для наследования от класса Singleton? Давайте вспомним, что изначальный вариант метода sharedInstance выглядит так:

+ (Singleton*) sharedInstance
{
    if (sharedSingleton_ == nil)
    {
        sharedSingleton_ = [[super allocWithZone:NULL] init];
    }
    return sharedSingleton_;
}

Если использовать трюк с NSAllocateObject для создания экземпляра, то он станет таким:

+ (Singleton *) sharedInstance
{
    if (sharedSingleton_ == nil)
    {
        sharedSingleton_ = [NSAllocateObject([self class], 0, NULL) init];
    }
    return sharedSingleton_;
}

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

Потокобезопасность

Класс Singleton в примере хорош только для общего использования. Если нужно использовать объект синглтона в многопоточной среде, то необходимо сделать его потокобезопасным. Для этого можно вставить блоки @synchronized() или использовать экземпляры NSLock вокруг проверки на nil для статической переменной экземпляра sharedSingleton_. Если есть какие-то другие свойства, которые тоже нужно защитить, то можно сделать их atomic.

Использование Синглтонов во фреймворке Cocoa Touch

В процессе знакомства с документацией Cocoa Touch Framework вам встретится много разных классов синглтонов. Мы поговорим о некоторых из них в этом разделе — UIApplication, UIAccelerometer и NSFileManager.

Использование класса UIApplication

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

Каждое приложение имеет единственный экземпляр UIApplication. Он создается как синглтон объект функцией UIApplicationMain при запуске приложения и доступен во время выполнения через метод класса sharedApplication.

Объект UIApplication выполняет много служебных задач для программы, включая начальную маршрутизацию входящих сообщений пользователя, а также диспетчеризацию сообщений о действиях для объектов класса UIControl к соответствующим целевым объектам. Он поддерживает список всех открытых объектов UIWindow. Объект приложения связан с объектом делегата приложения UIApplicationDelegate, который информируется о любых значимых событиях во время выполнения программы, таких, как запуск, предупреждения о нехватки памяти, завершение приложения и выполнение фоновых процессов. Обработчики этих событий дают возможность делегату настроить поведение приложения.

Использование класса UIAccelerometer

Другой распространенный синглтон во фреймворке Cocoa Touch – это UIAccelerometer. Класс UIAccelerometer позволяет приложению подписаться на получение связанных с акселерацией данных из встроенного акселерометра в iOS-устройстве. Приложение может использовать данные по изменениям линейной акселерации по главным осям в трехмерном пространстве для определения и текущей ориентации устройства, и мгновенных изменений в ориентации.
Класс UIAccelerometer является синглтоном, поэтому нельзя создавать его объекты явно. Для доступа к его единственному экземпляру нужно вызывать метод класса sharedAccelerometer. Кроме того, можно установить его свойство updateInterval и свойство delegate в свой собственный объект делегата для получения любых отчетных данных по акселерации от экземпляра синглтона.

Использование класса NSFileManager

Класс NSFileManager когда-то был «строгой» реализацией паттерна Синглтон перед Mac OS X 10.5 и в iOS 2.0. Вызов его метода init ничего не делает, и его единственный экземпляр может быть создан и доступен только через метод класса defaultManager. Однако поскольку синглтон реализация не является потокобезопасной, то приходится создавать новые экземпляры NSFileManager, чтобы обеспечить эту безопасность. Этот подход рассматривается как более гибкая реализация Синглтона, в которой фабричный метод всегда возвращает один и тот же экземпляр, но можно выделить и инициализировать также дополнительные экземпляры.

Если нужно реализовать «строгий» синглтон, необходима реализация, похожая на пример, описанный в предыдущих разделах. Иначе – не переопределяйте allocWithZone: и другие связанные методы.

Выводы

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

Автор: WildCat2013

Источник

Поделиться

* - обязательные к заполнению поля