{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Observer Pattern\n", "1. Behavioral\n", "1. Also known as **Publish Subscribe Pattern, Dependence Pattern**\n", "1. Used to control operation of Objects\n", "1. Used for Event Monitoring\n", "1. One to Many relationship builder, when one object's state changes, all dependents are notified\n", "\n", "> **Example**\n", "> - Newspaper Subscription\n", "> - Channel Subscription\n", "> - Any kind of push notification service\n", "> - Mostly used in GUIs\n", "\n", "**Advantages**\n", "1. Separation of Concern (Single Responsibility)\n", "1. Interface Segregation\n", "1. Open-Closed\n", "1. Dependency Inversion\n", "1. Encapsulate what varies \n", "\n", "> **MVC**\n", "> - Model View controller \n", "> - Model - Subject/Publisher \n", "> - View - Observer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Dashboard for Tech Support\n", "- KPI - Key Performance Indicators\n", " - Open Tickets\n", " - New Tickets in Last Hour\n", " - Closed Tickets in Last Hour\n", "- Observer - Dashboard, Perhaps History Viewer, Or Forecaster\n", "- Publisher(Subject) - KPI Source" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from abc import ABCMeta, abstractmethod" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Interfaces\n", "1. Context Managed - Lifecycle Method introduced, so that they clean up themselves and avoid Dangling References" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class IObserver(metaclass = ABCMeta):\n", " @abstractmethod\n", " def update(self, value):\n", " pass\n", " \n", " def __enter__(self):\n", " return self\n", " \n", " @abstractmethod\n", " def __exit__(self, exc_type, exc_value, traceback):\n", " pass" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class IPublisher(metaclass = ABCMeta):\n", " _observers = set()\n", " \n", " def attach(self, observer):\n", " if not isinstance(observer, IObserver):\n", " raise TypeError('Observer not derived from IObserver')\n", " self._observers |= {observer}\n", " \n", " def detach(self, observer):\n", " self._observers -= {observer}\n", " \n", " def notify(self, msg=None):\n", " for observer in self._observers:\n", " if msg is None:\n", " observer.update()\n", " else:\n", " observer.update(msg)\n", " \n", " def __enter__(self):\n", " return self\n", " \n", " def __exit__(self, exc_type, exc_value, traceback):\n", " self._observers.clear()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Implementation" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class KPIs(IPublisher):\n", " _open_tickets = -1\n", " _closed_tickets = -1\n", " _new_tickets = -1\n", " \n", " @property\n", " def open_tickets(self):\n", " return self._open_tickets\n", " \n", " @property\n", " def closed_tickets(self):\n", " return self._closed_tickets\n", " \n", " @property\n", " def new_tickets(self):\n", " return self._new_tickets\n", " \n", " def set_kpis(self, open_tickets, closed_tickets, new_tickets, msg=None):\n", " self._open_tickets = open_tickets\n", " self._closed_tickets = closed_tickets\n", " self._new_tickets = new_tickets\n", " \n", " self.notify(msg) " ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class CurrentKPIs(IObserver):\n", " open_tickets = -1\n", " closed_tickets = -1\n", " new_tickets = -1\n", " \n", " def __init__(self, kpis):\n", " self._kpis = kpis\n", " kpis.attach(self)\n", " \n", " def update(self, msg=None):\n", " self.open_tickets = self._kpis.open_tickets\n", " self.closed_tickets = self._kpis.closed_tickets\n", " self.new_tickets = self._kpis.new_tickets\n", " self.display(msg)\n", " \n", " def display(self, msg=None):\n", " print(f'Current KPIs ({msg}):\\nOpen: {self.open_tickets}'\n", " f'\\nClosed: {self.closed_tickets}\\nNew: {self.new_tickets}\\n')\n", " \n", " def __exit__(self, exc_type, exc_value, traceback):\n", " self._kpis.detach(self)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class ForecastKPIs(IObserver):\n", " open_tickets = -1\n", " closed_tickets = -1\n", " new_tickets = -1\n", " \n", " def __init__(self, kpis):\n", " self._kpis = kpis\n", " kpis.attach(self)\n", " \n", " def update(self, msg=None):\n", " self.open_tickets = self._kpis.open_tickets\n", " self.closed_tickets = self._kpis.closed_tickets\n", " self.new_tickets = self._kpis.new_tickets\n", " self.display(msg)\n", " \n", " def display(self, msg):\n", " print(f'Forecast KPIs ({msg}):\\nOpen: {self.open_tickets}'\n", " f'\\nClosed: {self.closed_tickets}\\nNew: {self.new_tickets}\\n')\n", " \n", " def __exit__(self, exc_type, exc_value, traceback):\n", " self._kpis.detach(self)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Driver Program" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Current KPIs (Good Performance):\n", "Open: 10\n", "Closed: 20\n", "New: 30\n", "\n", "Forecast KPIs (Good Performance):\n", "Open: 10\n", "Closed: 20\n", "New: 30\n", "\n", "=========================\n", "After Detaching\n", "=========================\n", "\n", "Forecast KPIs (Critical Performance):\n", "Open: 1\n", "Closed: 2\n", "New: 3\n", "\n" ] } ], "source": [ "with KPIs() as kpis:\n", " with CurrentKPIs(kpis) as currKPIs, ForecastKPIs(kpis):\n", " kpis.set_kpis(10, 20, 30, 'Good Performance')\n", " kpis.detach(currKPIs)\n", " print(\"=========================\\nAfter Detaching\\n=========================\\n\")\n", " kpis.set_kpis(1, 2, 3, 'Critical Performance')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### After Context Manager Exit\n", "1. No one is notified\n", "1. All the references has been removed from memory" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": true }, "outputs": [], "source": [ "kpis.set_kpis(100, 120, 160) # No more notifications fired" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Variation" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class IObservable(metaclass = ABCMeta):\n", " _observers = set()\n", " \n", " def subscribe(self, observer):\n", " self._observers |= {observer}\n", " \n", " def unsubscribe(self, observer):\n", " self._observers -= {observer}\n", " \n", " def emit(self, val):\n", " for observer in self._observers:\n", " observer(val)\n", " \n", " def __enter__(self):\n", " return self\n", " \n", " def __exit__(self, exc_type, exc_value, traceback):\n", " self._observers.clear()" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class NewKPIs(IObservable):\n", " _open_tickets = -1\n", " _closed_tickets = -1\n", " _new_tickets = -1\n", " \n", " def set_kpis(self, open_tickets, closed_tickets, new_tickets):\n", " self._open_tickets = open_tickets\n", " self._closed_tickets = closed_tickets\n", " self._new_tickets = new_tickets\n", " \n", " self.emit((self._open_tickets, self._closed_tickets, self._new_tickets)) " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def currKPI(val):\n", " x, y, z = val\n", " print(f'Current KPIs:\\nOpen: {x}\\nClosed: {y}\\nNew: {z}\\n')\n", " \n", "def foreKPI(val):\n", " x, y, z = val\n", " print(f'Forecast KPIs:\\nOpen: {x}\\nClosed: {y}\\nNew: {z}\\n')" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Forecast KPIs:\n", "Open: 1\n", "Closed: 2\n", "New: 3\n", "\n", "Current KPIs:\n", "Open: 1\n", "Closed: 2\n", "New: 3\n", "\n", "=========================\n", "After Detaching\n", "=========================\n", "\n", "Forecast KPIs:\n", "Open: 10\n", "Closed: 20\n", "New: 30\n", "\n" ] } ], "source": [ "with NewKPIs() as newKPIs:\n", " newKPIs.subscribe(currKPI)\n", " newKPIs.subscribe(foreKPI)\n", " newKPIs.set_kpis(1, 2, 3)\n", " newKPIs.unsubscribe(currKPI)\n", " print(\"=========================\\nAfter Detaching\\n=========================\\n\")\n", " newKPIs.set_kpis(10, 20, 30)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.2" } }, "nbformat": 4, "nbformat_minor": 2 }