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

}

П. С.
Хочу попросити всіх читачів і коментаторів: якщо ви побачили якусь неточність чи знаєте, як який-небудь із наведених мною шматків коду написати правильніше/красивіше/коректніше — скажіть про це. Я пишу тут зовсім не для того, щоб вчити інших, а для того, щоб навчитися самому. Адже поки ми вчимося — ми залишаємося молодими.
Дякуємо вам за увагу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.