Паттерны проектирования, взгляд iOS разработчика. Часть 2. Наблюдатель

от автора

Содержание:

Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Часть 2. Наблюдатель

Сегодня мы разберемся с "начинкой" паттерна "Наблюдатель". Сразу оговорюсь, что в мире iOS у вас не будет острой необходимости реализовывать этот паттерн, поскольку в SDK уже есть NotificationCenter. Но в образовательных целях мы полностью разберем анатомию и применение этого паттерна. К тому же, самостоятельная реализация может обладать большей гибкостью и, в некоторых случаях, быть более полезной.

"Кажется дождь собирается" (с)

Авторы книги "Паттерны проектирования" (Эрик и Элизабет Фримен), в качестве примера, предлагают применять паттерн "Наблюдатель" к разработке приложения Weather Station. Представьте, что у нас есть: метеостанция, и объект WeatherData, который обрабатывает данные от ее датчиков и передает их нам. Приложение же состоит из трех экранов: экрана текущего состояния погоды, экрана статистики и экрана прогноза.

Мы знаем, что WeatherData предоставляет нам такой интерфейс:

// Objective-C - (double)getTemperature; - (double)getHumidity; - (double)getPressure; - (void)measurementsChanged;

// Swift func getTemperature() -> Double func getHumidity() -> Double func getPressure() -> Double func measurementsChanged()

Также разработчики WeatherData сообщили, что при каждом обновлении погодных датчиков будет вызван метод measurementsChanged.

Конечно же, самое простое решение — написать код непосредственно в этом методе:

// Objective-C - (void)measurementsChanged {     double temp = [self getTemperature];     double humidity = [self getHumidity];     double pressure = [self getPressure];      [currentConditionsDisplay updateWithTemp:temp humidity:humidity andPressure:pressure];     [statisticsDisplay updateWithTemp:temp humidity:humidity andPressure:pressure];     [forecastDisplay updateWithTemp:temp humidity:humidity andPressure:pressure]; }

// Swift func measurementsChanged() {     let temp = self.getTemperature()     let humidity = self.getHumidity()     let pressure = self.getPressure()      currentConditionsDisplay.update(with: temp, humidity: humidity, and: pressure)     statisticsDisplay.update(with: temp, humidity: humidity, and: pressure)     forecastDisplay.update(with: temp, humidity: humidity, and: pressure) }

Такой подход конечно же плох, потому что:

  • программируем на уровне конкретных реализаций;
  • сложная расширяемость в будущем;
  • нельзя в рантайме добавлять/убирать экраны, на которых будет показана информация;
  • … (свой вариант);

Поэтому паттерн "Наблюдатель" будет в этой ситуации очень кстати. Поговорим немного о характеристиках этого паттерна.

«Наблюдатель». Что под капотом?

Основные характеристики этого паттерна — наличие СУБЪЕКТА и, собственно, НАБЛЮДАТЕЛЕЙ. Связь, как вы уже догадались, один ко многим, и при изменении состояния СУБЪЕКТА происходит оповещение его НАБЛЮДАТЕЛЕЙ. На первый взгляд все просто.

Первое что нам понадобится — интерфейсы (протоколы) для наблюдателей и субъекта:

// Objective-C @protocol Observer <NSObject>  - (void)updateWithTemperature:(double)temperature                      humidity:(double)humidity                   andPressure:(double)pressure;  @end  @protocol Subject <NSObject>  - (void)registerObserver:(id<Observer>)observer; - (void)removeObserver:(id<Observer>)observer; - (void)notifyObservers;  @end

// Swift protocol Observer: class {     func update(with temperature: Double, humidity: Double, and pressure: Double) }  protocol Subject: class {     func register(observer: Observer)     func remove(observer: Observer)     func notifyObservers() }

Теперь нужно привести в порядок WeatherData (подписать на соотв. протокол и не только):

// Objective-C  // файл заголовка WeatherData.h @interface WeatherData : NSObject <Subject>  - (void)measurementsChanged; - (void)setMeasurementWithTemperature:(double)temperature                              humidity:(double)humidity                           andPressure:(double)pressure; // test method  @end  // файл реализации WeatherData.m @interface WeatherData()  @property (strong, nonatomic) NSMutableArray<Observer> *observers; @property (assign, nonatomic) double temperature; @property (assign, nonatomic) double humidity; @property (assign, nonatomic) double pressure;  @end  @implementation WeatherData  - (instancetype)init {     self = [super init];     if (self) {         self.observers = [[NSMutableArray<Observer> alloc] init];     }     return self; }  - (void)registerObserver:(id<Observer>)observer {     [self.observers addObject:observer]; }  - (void)removeObserver:(id<Observer>)observer {     [self.observers removeObject:observer]; }  - (void)notifyObservers {     for (id<Observer> observer in self.observers) {         [observer updateWithTemperature:self.temperature                                humidity:self.humidity                             andPressure:self.pressure];     } }  - (void)measurementsChanged {     [self notifyObservers]; }  - (void)setMeasurementWithTemperature:(double)temperature                              humidity:(double)humidity                           andPressure:(double)pressure {      self.temperature = temperature;     self.humidity = humidity;     self.pressure = pressure;     [self measurementsChanged]; }  @end

// Swift class WeatherData: Subject {      private var observers: [Observer]     private var temperature: Double!     private var humidity: Double!     private var pressure: Double!      init() {         self.observers = [Observer]()     }      func register(observer: Observer) {         self.observers.append(observer)     }      func remove(observer: Observer) {         self.observers = self.observers.filter { $0 !== observer }     }      func notifyObservers() {         for observer in self.observers {             observer.update(with: self.temperature, humidity: self.humidity, and: self.pressure)         }     }      func measurementsChanged() {         self.notifyObservers()     }      func setMeasurement(with temperature: Double,                         humidity: Double,                         and pressure: Double) { // test method          self.temperature = temperature         self.humidity = humidity         self.pressure = pressure         self.measurementsChanged()     }  }

Мы добавили тестовый метод setMeasurement для имитации изменения состояний датчиков.

Поскольку методы register и remove у нас редко будут меняться от субъекта к субъекту, было бы хорошо иметь их реализацию по умолчанию. В Objective-C для этого нам понадобится дополнительный класс. Но для начала переименуем наш протокол и уберем из него эти методы:

// Objective-C @protocol SubjectProtocol <NSObject>  - (void)notifyObservers;  @end

Теперь добавим класс Subject:

// Objective-C  // файл заголовка Subject.h @interface Subject : NSObject  @property (strong, nonatomic) NSMutableArray<Observer> *observers;  - (void)registerObserver:(id<Observer>)observer; - (void)removeObserver:(id<Observer>)observer;  @end  // файл реализации Subject.m @implementation Subject  - (void)registerObserver:(id<Observer>)observer {     [self.observers addObject:observer]; }  - (void)removeObserver:(id<Observer>)observer {     [self.observers removeObject:observer]; }  @end

Как видите, в этом классе два метода и массив наших наблюдателей. Теперь в классе WeatherData убираем этот массив из свойств и унаследуемся от Subject, а не от NSObject:

// Objective-C @interface WeatherData : Subject <SubjectProtocol>

В свифте, благодаря расширениям протоколов, дополнительный класс не понадобится.
Мы просто включим в протокол Subject свойство observers:

// Swift protocol Subject: class {     var observers: [Observer] { get set }      func register(observer: Observer)     func remove(observer: Observer)     func notifyObservers() }

А в расширении протокола напишем реализацию методов register и remove по умолчанию:

// Swift extension Subject {      func register(observer: Observer) {         self.observers.append(observer)     }      func remove(observer: Observer) {         self.observers = self.observers.filter {$0 !== observer }     }  }

Принимаем сигналы

Теперь нам нужно реализовать экраны нашего приложения. Мы реализуем только один из них: CurrentConditionsDisplay. Реализация остальных аналогична.

Итак, создаем класс CurrentConditionsDisplay, добавляем в него два свойства и метод display (этот экран должен показывать текущее состояние погоды, как мы помним):

// Objective-C @interface CurrentConditionsDisplay()  @property (assign, nonatomic) double temperature; @property (assign, nonatomic) double humidity;  @end  @implementation CurrentConditionsDisplay  - (void)display {     NSLog(@"Current conditions: %f degrees and %f humidity", self.temperature, self.humidity); }  @end

// Swift private var temperature: Double! private var humidity: Double!  func display() {     print("Current conditions: \(self.temperature) degrees and \(self.humidity) humidity") }

Теперь нам нужно "подписать" этот класс на протокол Observer и реализовать необходимый метод:

// Objective-C  // в файле заголовка CurrentConditionsDisplay.h @interface CurrentConditionsDisplay : NSObject <Observer>  // в файле реализации CurrentConditionsDisplay.m - (void)updateWithTemperature:(double)temperature                      humidity:(double)humidity                   andPressure:(double)pressure {      self.temperature = temperature;     self.humidity = humidity;     [self display]; }

// Swift class CurrentConditionsDisplay: Observer {      func update(with temperature: Double, humidity: Double, and pressure: Double) {         self.temperature = temperature         self.humidity = humidity         self.display()     }

Почти готово. Осталось зарегистрировать нашего наблюдателя у субъекта (также не забывайте удалять регистрацию при деинициализации).

Для этого нам понадобится еще одно свойство:

// Objective-C @property (weak, nonatomic) Subject<SubjectProtocol> *weatherData;

// Swift private weak var weatherData: Subject?

И инициализатор с деинициализатором:

// Objective-C - (instancetype)initWithSubject:(Subject<SubjectProtocol> *)subject {     self = [super init];     if (self) {         self.weatherData = subject;         [self.weatherData registerObserver:self];     }     return self; }  - (void)dealloc {     [self.weatherData removeObserver:self]; }

// Swift init(with subject: Subject) {     self.weatherData = subject     self.weatherData?.register(observer: self) }  deinit {     self.weatherData?.remove(observer: self) }

Заключение

Мы написали довольно простую реализацию паттерна "Наблюдатель". Наш вариант, конечно же не без изъянов. Например, если мы добавим четвертый датчик, то нужно будет переписывать интерфейс наблюдателей и реализации этого интерфейса (чтоб доставлять до наблюдателей четвертый параметр), а это не есть хорошо. В NotificationCenter, о котором я упоминал в самом начале статьи, такой проблемы не существует. Дело в том, что там передача данных происходит одним-единым параметром-словарем.

Спасибо за внимание, учитесь и учите других.
Ведь пока мы учимся — мы остаемся молодыми. 🙂

ссылка на оригинал статьи https://habrahabr.ru/post/324292/