July 4, 2024

74. Делаем свой NotificationCenter

На одном из собеседований мне дали задачу сделать свой NotificationCenter c memory safety, type safety и thread safety. В этой статье покажу вариант реализации задачи, чтобы вам не пришлось снова изобретать 🛞

iPhone, уведомления, колесо

Постановка

Необходимо сделать свой аналог 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, то нужно обязательно уточнить, не издевается ли над вами коллега, и если не издевается, то нужно получить аргументы в пользу такого решения.

Не принимаются такие аргументы:

  • потому что так надо (почему? для чего?)
  • потому что так лучше (чем лучше?)
  • потому что мы так решили (кто мы, и почему?)
  • потому что стандартный не решает наши задачи (какие?)
  • не знаю, задача пришла не от меня, а от лида (отправляем коллегу к лиду, или идем сами)

Заключение

Кто бы ни придумывал такие задачи, надеюсь, им хорошо работается и отдыхается 😅

По моему опыту на собесах достаточно часто встречаются кандидаты, которые не знают, почему используют в работе классы, а не структуры - и вот это я считаю более важной темой для общения на собесе, чем изобретать кастомные нотификейшен-центры.

Если статья хоть немного вас повеселила, это успех!

Код для этой статьи можно посмотреть тут, а другие статьи - тут.


Кроме мобильной разработки я изучаю/практикую инвестиции, об этом можно почитать тут.