{
 "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
}