Содержание:


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


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

Поделиться с друзьями
-->

Комментарии (30)


  1. svistkovr
    23.03.2017 14:29
    -2

    Чем отличается ваш самописный паттерн от системного addObserver?


    1. s_suhanov
      23.03.2017 21:15

      Так какбэ ничем. Я ж для этого и использовал в статье такие ключевые слова как "не будет острой необходимости", "NotificationCenter" и "в образовательных целях". Причем в первом же абзаце. )))


  1. sapphire_id
    23.03.2017 18:06
    -1

    В NotificationCenter, о котором я упоминал в самом начале статьи, такой проблемы не существует. Дело в том, что там передача данных происходит одним-единым параметром-словарем.


    Извините, а почему нельзя сделать такой же параметр-словарь вместо нескольких параметров в методе
    - (void)updateWithTemperature:(double)temperature
                         humidity:(double)humidity
                      andPressure:(double)pressure;
    
    ?


    1. s_suhanov
      23.03.2017 21:16

      Можно конечно. Но если внимательно прочитать статью сначала, то выяснится, что это лишь учебный пример, который подан в таком виде лишь для лучшего понимания материала. :)


      1. sapphire_id
        23.03.2017 23:07
        -1

        Я прекрасно понимаю, что ваш пример приведен для лучшего разъяснения паттерна и в реальном мире для этих целей есть решение из SDK. Вопрос ведь об этом.

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


        1. tapoton
          24.03.2017 11:56
          +3

          Использование словаря также не лучшее решение.

          В случае с методами вы знаете о типах данных, не нужно бояться, что если вдруг тип одного из передаваемых значений изменится, то вы получите краш или просто некорректный вывод на экране: лучше получить ошибки на этапе компиляции и поправить их в каждом месте, чем править их по мере «натыкания» на краш или, что еще сложнее заметить, на пустой лэйбл, заходя на один из экранов раз в месяц, поскольку его разработкой сейчас не занимаетесь.

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

          А если наблюдатель, простите за тавтологию, наблюдает за значениями/объектами/событиями, не относящимися к одной группе, так вообще лучше объявить несколько методов в протоколе наблюдателя.


          1. NoFearJoe
            24.03.2017 14:17

            Использование словаря — универсальное решение.
            Можно ведь создать тот же самый WeatherDescription и написать в нем 2 метода:

            toDictionary() -> [String: Any]
            
            и
            init?(from dictionary: [String: Any])
            

            И получишь строгую типизацию, при этом оставив саму реализацию Observer универсальной.


            1. Sayonji
              26.03.2017 16:50
              +2

              Насколько я понимаю, это не поможет с ловлей ошибок на этапе компиляции.


  1. NoFearJoe
    23.03.2017 22:23
    +1

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


    1. s_suhanov
      24.03.2017 14:44

      "Батя, я стараюся" (с) :)


  1. pilotua
    26.03.2017 11:48
    +1

    спасибо за статью, но владением должен заниматься наблюдатель. Ведь если мы начинаем наблюдать, значит важно что бы наблюдаемый не подох раньше времени.


    1. s_suhanov
      26.03.2017 12:09

      Хммм. А напишите конкретную реализацию для WeatherStation плиз.


      1. pilotua
        26.03.2017 12:15

        вы можете в наблюдаемом объекте вместо массива с наблюдателями использовать NSHashTable, у нее есть метод +weakObjectsHashTable.


        1. anticyclope
          26.03.2017 18:17

          NSPointerArray тоже подойдет


        1. s_suhanov
          26.03.2017 18:19

          Так в наблюдаемом объекте нет массива с наблюдателями, как можно что-то использовать "вместо" него? :)


          1. pilotua
            26.03.2017 21:06
            +1

            как же нет?

            @property (strong, nonatomic) NSMutableArray *observers;


            1. s_suhanov
              26.03.2017 21:33

              Сорри, это я туплю. :)


  1. sbnur
    26.03.2017 11:57
    -1

    Мудреть лучше, чем молодеть


    1. s_suhanov
      26.03.2017 18:20

      То же самое, что сказать "лучше быть теплым чем мягким" ;)


      Какое, по вашему мнению, отношение имеют друг к другу слова "старый" и "мудрый"? :)


      1. sbnur
        26.03.2017 18:25
        -1

        а причем тут старый — я про старый не писал


        1. s_suhanov
          26.03.2017 18:30

          Уточню вопрос, так чтоб он стал более понятен: почему вы считаете, что нельзя мудреть и молодеть одновременно? :)


          1. sbnur
            26.03.2017 19:03
            -1

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


  1. anticyclope
    26.03.2017 18:36

    NSMutableArray — это вообще законно? :)


    1. anticyclope
      26.03.2017 19:03
      +1

      Только что понял, что парсер съел угловые скобки.
      Имелось ввиду, что

      NSMutableArray<Observer>
      это не то же самое, что
      NSMutableArray<id<Observer>>


      1. s_suhanov
        26.03.2017 19:05

        Проясните в чем отличие?


        1. anticyclope
          26.03.2017 19:09
          +1

          В первом случае это будет свойство «NSMutableArray, адоптящий протокол Observer», во втором — «NSMutableArray, элементы которого теоретически адоптят Observer»


          1. s_suhanov
            26.03.2017 19:12

            Аааа, кажется понял. То есть — в первом случае, это может быть какой-нибудь объект, который мы унаследовали от NSMutableArray и подписали на протокол Observer? Но не обязательно, что его элеметы будут следовать этому протоколу?


            1. anticyclope
              26.03.2017 19:15
              +1

              Да, все верно. Ну и за компанию: в данном случае торчать мутабельностью вредно. Торчать не ридонли мутабельностью вредно вдвойне. Обязательно найдется кто-то, кто «пофиксит» баг обнулением/перезаписью свойства, либо вызовом removeAllObjects.


              1. s_suhanov
                26.03.2017 19:17
                +1

                В общем: и первый комент тоже имеет смысл:


                NSMutableArray — это вообще законно? :)

                Спасибо, люблю каменты по делу. Теперь буду это знать, благодаря вам. :)


              1. pilotua
                26.03.2017 21:18

                так мутабельное свойство в приватном екстеншене, или пофиксили уже?