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


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

    Поделиться публикацией

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

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

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

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


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

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

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

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

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

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

              А если наблюдатель, простите за тавтологию, наблюдает за значениями/объектами/событиями, не относящимися к одной группе, так вообще лучше объявить несколько методов в протоколе наблюдателя.
                0
                Использование словаря — универсальное решение.
                Можно ведь создать тот же самый WeatherDescription и написать в нем 2 метода:
                toDictionary() -> [String: Any]
                
                и
                init?(from dictionary: [String: Any])
                

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

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

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

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

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

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

                      +1
                      как же нет?
                      @property (strong, nonatomic) NSMutableArray *observers;
                        0

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

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

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


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

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

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

                        –1
                        Поясню также для понятности: обучение, как получение знаний, в соединение с практическим опытом должно привести к мудрости (знания без мудрости опасны, не только на мой взгляд).
                        На это и обращено внимание.
                        Молодение, старение и прочее возрастное изменение с процессом обучения связано косвенно, опять же на мой взгляд
                        Для понятности — есть русская пословица. отражающая связь учения с временем — Век живи, век учись — дураком помрешь
                  0
                  NSMutableArray — это вообще законно? :)
                    +1
                    Только что понял, что парсер съел угловые скобки.
                    Имелось ввиду, что
                    NSMutableArray<Observer>
                    это не то же самое, что
                    NSMutableArray<id<Observer>>
                      0

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

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

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

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

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


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

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

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

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое