Паттерны проектирования, взгляд iOS разработчика. Часть 0. Синглтон-Одиночка
Я почув і забув.
Я записав і запам'ятав.
Я зробив і зрозумів.
Я навчив іншого, тепер я майстер.
(В.В. Бублик)
Небольшое вступление.
Я не зря вынес в начало поста цитату на украинском языке. Дело в том, что именно эти слова я услышал от своего преподавателя программирования на втором курсе университета, и именно в таком виде я вспоминаю эти слова до сих пор. Как вы можете догадаться, эта цитата является отсылкой к высказыванию Конфуция, но в ней есть очень важное дополнение о достижении мастерства.
И именно эти слова и сподвигли меня на написание данной серии постов. Дело в том, что я — начинающий iOS разработчик, и я очень хочу разобраться в паттернах проектирования. И я не придумал лучшего способа, чем взять книгу «Паттерны проектирования» Эрика и Элизабет Фримен, и написать примеры каждого паттерна на Objective-C и Swift. Таким образом я смогу лучше понять суть каждого паттерна, а также особенности обоих языков.
Итак, начнем с самого простого на мой взгляд паттерна.
Одиночка, он же — синглтон.
Основная задача синглтона — предоставить пользователю один и только один объект определенного класса на весь жизненный цикл приложения. В 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() {}
}
П.С.
Хочу попросить всех читателей и комментаторов: если вы увидели какую-нибудь неточность или знаете как какой-либо из приведенных мною кусков кода написать правильнее/красивее/корректнее — скажите об этом. Я пишу здесь совсем не для того, чтоб учить других, а для того, чтоб научиться самому. Ведь пока мы учимся — мы остаемся молодыми.
Спасибо вам за внимание.