Pull to refresh

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

Reading time 7 min
Views 12K

Содержание:


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


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

Tags:
Hubs:
+5
Comments 30
Comments Comments 30

Articles