Содержание:
Часть 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/
Добавить комментарий