74. Делаем свой NotificationCenter
На одном из собеседований мне дали задачу сделать свой NotificationCenter c memory safety, type safety и thread safety. В этой статье покажу вариант реализации задачи, чтобы вам не пришлось снова изобретать 🛞
Постановка
Необходимо сделать свой аналог NotificationCenter, который будет соответствовать таким качествам: memory safety, type safety и thread safety.
У нового инструмента должны быть методы для:
- регистрации наблюдателя
- удаления наблюдателя
- публикации уведомления
- проверки на наличие события среди наблюдателей
Решение
import Foundation
final class CustomNotificationCenter {
/// Синглтон по аналогии со стандартным `NotificationCenter`
static let shared = CustomNotificationCenter()
/// Словарь для хранения наблюдателей, где ключ - имя события,
/// а значение - словарь с токенами наблюдателей и экшенами
private var observers = [String: [UUID: (Any) -> Void]]()
/// Очередь для обеспечения потокобезопасного доступа к данным
private let queue = DispatchQueue(label: "com.yourapp.notificationcenter")
// Приватный инициализатор, чтобы предотвратить создание экземпляров класса извне
private init() {}
/// Метод для добавления наблюдателя на событие с заданным именем
func addObserver(forName name: String, using action: @escaping (Any) -> Void) -> UUID {
// Создаем уникальный токен для наблюдателя
let token = UUID()
// Синхронизируем доступ к словарю наблюдателей, чтобы избежать race conditions
queue.sync {
// Если для данного события еще нет словаря наблюдателей, создаем новый
if self.observers[name] == nil {
self.observers[name] = [:]
}
// Добавляем новый наблюдатель в словарь с токеном в качестве ключа и экшеном в качестве значения
self.observers[name]?[token] = action
}
// Возвращаем токен наблюдателя для возможности его удаления в будущем
return token
}
// Метод для отправки уведомления с заданным именем и объектом
func postNotification(withName name: String, object: Any) {
// Асинхронно выполняем отправку уведомлений
queue.async {
// Получаем словарь наблюдателей для данного события
if let observers = self.observers[name] {
// Вызываем экшены всех зарегистрированных наблюдателей
for observer in observers.values {
observer(object)
}
}
}
}
// Метод для удаления наблюдателя с заданным токеном и именем события
func removeObserver(withToken token: UUID, forName name: String) {
// Синхронизируем доступ к словарю наблюдателей
queue.sync {
// Удаляем наблюдатель из словаря по токену
self.observers[name]?.removeValue(forKey: token)
}
}
// Метод для проверки наличия зарегистрированных наблюдателей для события с заданным именем
func hasEvent(withName name: String) -> Bool {
var hasEvent = false
// Синхронизируем доступ к словарю наблюдателей
queue.sync {
// Проверяем, что словарь наблюдателей для данного события существует и не пуст
hasEvent = self.observers[name]?.isEmpty == false
}
return hasEvent
}
}
Применение
// 1 - Добавление наблюдателя на событие "CustomNotification"
let token = CustomNotificationCenter.shared.addObserver(forName: "CustomNotification") { object in
if let message = object as? String {
print("Получили уведомление с сообщением: \(message)")
}
}
// 2 - Отправка уведомления с именем "CustomNotification" и объектом "Hello, World!"
CustomNotificationCenter.shared.postNotification(withName: "CustomNotification", object: "Hello, World!")
// 3 - Проверка наличия зарегистрированных наблюдателей для события "CustomNotification"
if CustomNotificationCenter.shared.hasEvent(withName: "CustomNotification") {
print("Событие 'CustomNotification' имеет зарегистрированных наблюдателей")
} else {
print("Событие 'CustomNotification' не имеет зарегистрированных наблюдателей")
}
// 4 - Удаление наблюдателя по токену
CustomNotificationCenter.shared.removeObserver(withToken: token, forName: "CustomNotification")
(1) Добавление наблюдателя
- Вызывается метод addObserver(forName:using:), передавая имя события "CustomNotification" и блок кода, который будет вызван при получении уведомления
- Метод возвращает уникальный токен наблюдателя, который можно использовать для удаления наблюдателя в будущем.
(2) Отправка уведомления
- Вызывается метод
postNotification(withName:object:), передавая имя события "CustomNotification" и объект, который будет передан наблюдателям. - Этот метод асинхронно вызывает блоки кода всех зарегистрированных наблюдателей для данного события.
(3) Проверка наличия зарегистрированных наблюдателей
- Вызывается метод
hasEvent(withName:), передавая имя события "CustomNotification". - Метод возвращает
true, если для данного события есть зарегистрированные наблюдатели, иfalseв противном случае.
(4) Удаление наблюдателя
- Вызывается метод
removeObserver(withToken:forName:), передавая токен наблюдателя, полученный при добавлении, и имя события "CustomNotification". - Этот метод удаляет наблюдателя из словаря наблюдателей для данного события.
Примечания
Вероятно, данная задача придумана с целью проверить навыки кандидата по работе с очередями, словарями и синглтоном, но это лишь моя догадка.
В 100% случаев вам не придется делать такое в рабочем порядке, потому что у нас есть славный NotificationCenter, который можно и нужно использовать в самых разных сценариях.
Если же в работе вас попросили сделать свой аналог NotificationCenter, то нужно обязательно уточнить, не издевается ли над вами коллега, и если не издевается, то нужно получить аргументы в пользу такого решения.
Не принимаются такие аргументы:
- потому что так надо (почему? для чего?)
- потому что так лучше (чем лучше?)
- потому что мы так решили (кто мы, и почему?)
- потому что стандартный не решает наши задачи (какие?)
- не знаю, задача пришла не от меня, а от лида (отправляем коллегу к лиду, или идем сами)
Заключение
Кто бы ни придумывал такие задачи, надеюсь, им хорошо работается и отдыхается 😅
По моему опыту на собесах достаточно часто встречаются кандидаты, которые не знают, почему используют в работе классы, а не структуры - и вот это я считаю более важной темой для общения на собесе, чем изобретать кастомные нотификейшен-центры.
Если статья хоть немного вас повеселила, это успех!
Код для этой статьи можно посмотреть тут, а другие статьи - тут.
Кроме мобильной разработки я изучаю/практикую инвестиции, об этом можно почитать тут.