Содержание:
Часть 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
, о котором я упоминал в самом начале статьи, такой проблемы не существует. Дело в том, что там передача данных происходит одним-единым параметром-словарем.
Спасибо за внимание, учитесь и учите других.
Ведь пока мы учимся — мы остаемся молодыми. :)