Найти в Дзене
ZDG

Шаблоны проектирования: Наблюдатель

Предыдущие части: Визитёр, Фабрика, Синглтон, Стратегия, MVC, Вступление Наблюдатель (Observer) – хороший паттерн, и даже если вы его не используете, на самом деле вы его используете, потому что он вшит, например, в систему событий браузера. Какая задача решается? Шаблон формально описан так, что вроде бы и понятен, но в то же время непонятен: Объект, называемый "субъект", имеет список других объектов, называемых "наблюдателями", и извещает наблюдателей в случае изменения своего состояния. Это описание вряд ли скажет нам о том, где и зачем нужно применять такой шаблон, потому что для этого нужно, чтобы сначала возникла такая необходимость, а возникает она не всегда. Время аналогий! У вас есть 100 друзей, и вы собрались куда-то поехать. Все друзья уже готовы и ждут только вас одного. Выражаясь формально, они ждут, когда ваше состояние изменится. Каждую минуту каждый из 100 друзей звонит вам и проверяет ваше состояние, спрашивая "Ну ты едешь"? Итого за 10 минут вы получите 1000 звонков.
Оглавление

Предыдущие части: Визитёр, Фабрика, Синглтон, Стратегия, MVC, Вступление

Наблюдатель (Observer) – хороший паттерн, и даже если вы его не используете, на самом деле вы его используете, потому что он вшит, например, в систему событий браузера.

Какая задача решается?

Шаблон формально описан так, что вроде бы и понятен, но в то же время непонятен:

Объект, называемый "субъект", имеет список других объектов, называемых "наблюдателями", и извещает наблюдателей в случае изменения своего состояния.

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

Время аналогий!

У вас есть 100 друзей, и вы собрались куда-то поехать. Все друзья уже готовы и ждут только вас одного.

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

Каждую минуту каждый из 100 друзей звонит вам и проверяет ваше состояние, спрашивая "Ну ты едешь"?

Итого за 10 минут вы получите 1000 звонков. И каждый друг позвонит вам по 10 раз.

В конце концов это вам надоедает и вы говорите друзьям – не надо мне больше звонить. Когда я буду готов, я сам вам позвоню.

И теперь, как только вы стали готовы, вы делаете 100 звонков и оповещаете каждого друга о том, что ваше состояние изменилсь.

По сравнению с предыдущей схемой выгоды очевидны:

  1. Требуется совершить гораздо меньше звонков
  2. Друзья узнают об изменении вашего состояния именно тогда, когда оно изменится
  3. Вас никто не отвлекает запросами, пока вы готовитесь
  4. Друзьям тоже не надо отвлекаться, чтобы постоянно проверять вас

В переложении на программный код это значит, что программа будет работать более эффективно.

Реализация

Объект-субъект должен иметь методы addObserver(), removeObserver() и notifyObservers().

Основная фишка в том, что субъект хранит у себя список объектов-наблюдателей, которые за ним наблюдают. В ситуации с телефонными звонками вы – субъект, и у вас должна быть записная книжка, где вы храните 100 телефонов, на которые надо позвонить.

Наблюдатели добавляются и удаляются по своему желанию. То есть к вам приходит кто-то через метод addObserver() и говорит: добавь меня. Вы добавляете его в список. Кто-то приходит через метод removeObserver() и говорит: удали меня. Вы вычёркиваете его из списка.

Как можно заметить, вам до некоторой степени всё равно, кто приходит.

Главное, чтобы ему можно было позвонить. Для этого у него должен существовать метод update(), который вы и будете вызывать. (Тут обычный прогон про интерфейсы и прочее).

Итого:

  • Наблюдатели добавляются в список к субъекту
  • Субъект дожидается изменения состояния и после этого вызывает свой метод notifyObservers(), в котором идёт по списку и у каждого наблюдателя вызывает метод update().
А вы заметили, что дедушка Камадзи применяет шаблон "Наблюдатель"? На самом деле не совсем, но мы с этим разберемся дальше.
А вы заметили, что дедушка Камадзи применяет шаблон "Наблюдатель"? На самом деле не совсем, но мы с этим разберемся дальше.

Осталось понять, где именно в программе можно применять такую схему?

Событийно-ориентированная система

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

Посмотрим, как это происходит. Есть, грубо говоря, два типа программ.

Первый тип это постоянно "занятая" программа. Например, вы играете в видеоигру, где что-то бегает и взрывается, и она постоянно перерисовывает экран. В каждый момент состояние игры требует обновления.

Поэтому такая "занятость" вполне естественна и оправдана.

Второй тип – это где без внешнего воздействия ничего не меняется. Возьмём опять же игру, но особенную – типа крестиков-ноликов, которая ждёт нашего хода, и пока она ждёт, экран перерисовывать не нужно, так как состояние не меняется.

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

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

Практическое применение

Я очень долго думал над примерами, но все они оказались несостоятельны, поэтому я сдался.

В качестве неудачного примера возьмем ранее написанную игру Robots и разберём, почему он неудачный.

С первого взгляда Robots идеально подходит под шаблон. Во-первых, она пошаговая, то есть состояние меняется только от внешнего воздействия. Во-вторых, там прямо-таки идеальный субъект (игрок) и наблюдатели (роботы).

Стало быть, шаблон реализовать элементарно. Субъект-игрок хранит список наблюдателей-роботов у себя. При изменении состояния (игрок переходит в соседнюю клетку) он оповещает наблюдателей, вызывая у них метод update().

Казалось бы, всё отлично. Так почему этот пример плохой?

Потому что в данном случае совершенно излишне применять шаблон.

Применяя его, мы соглашаемся с тем, что каждый робот является объектом и обладает своим "сознанием", которое работает внутри метода update(). Но у роботов очень простое поведение – они просто движутся по направлению к игроку. Поэтому, перебирая роботов в цикле, можно двигать их прямо сразу, не прибегая к методу update(). И игроку совершенно незачем (и даже как-то противоестественно) оповещать их – когда он сдвинулся, знает сама программа, и в этот момент она может перебрать роботов. Иначе говоря, вместо изолированных "сознаний" внутри каждого робота здесь уместнее сделать центральное "сознание" для самой программы, которая будет ворошить роботов просто как массив данных.

Однако, вопрос этот весьма философский, и будет зависеть от многих факторов.

Так как в программе мы 1) ждём события 2) перебираем роботов в цикле и 3) обрабатываем каждого робота, это значит, что мы всё-таки воспроизводим шаблон Наблюдатель, просто в неявном виде. Субъектом является вся программа.

Особенности реализации

При всех достоинствах, у Наблюдателя есть и недостаток. Связан он с тем, что наблюдатель может не получить оповещение, или получить, но не обработать его. Конечно, так быть не должно, и мы говорим скорее о теоретическом случае. Вы позвонили другу, чтобы его оповестить, а у него в этот момент был занят телефон.

Пропустив ваш звонок, друг уже никогда не узнает, что вы готовы. Он продолжит ждать до бесконечности, или до тех пор, пока вы не оповестите его ещё раз.

Сравните с вариантом, когда друзья сами названивают вам: даже если звонок сорвётся, друг сразу же перезвонит снова. И таким образом он будет всегда в курсе вашего состояния.

Что дальше?

Данный шаблон естественным образом перерождается в следующий – назову его пока загадочным именем ПабСаб. И разберём его в следующем выпуске.

Читайте дальше: