{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "59ecb880",
   "metadata": {},
   "source": [
    "# Композиция классов #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0953a7c",
   "metadata": {},
   "source": [
    "В предыдущей лекции мы рассмотрели, как работает наследование и множественное наследование в классах Python. Как я уже говорил, если создать достаточно большую иерархию классов и использовать множественное наследование, а также большое количество классов-примесей, то итоговый код может показаться очень сложным, и его будет очень трудно читать программисту и разбираться, как он работает, что делает его менее выразительным.\n",
    "\n",
    "В Python существует альтернативный подход наследованию — это композиция.\n",
    "\n",
    "В этой лекции мы с вами на примере разберем, как работает композиция, и вы сможете сравнить, какой из подходов вам больше нравится — наследование или композиция.\n",
    "\n",
    "Давайте немного вспомним классы. У нас был класс \"питомец\", мы от него унаследовали класс \"собачку\" (класс `Dog`). Затем мы захотели, чтобы наши объекты классов \"собака\" могли выполнять экспорт данных, и мы ввели класс-примесь `ExportJSON`. После этого наш финальный класс `ExDog` использовал множественное наследование и наследовался от класса \"собачка\" и `ExportJSON`, тем самым он решал все наши задачи."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "7abf9ef1",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Pet:\n",
    "    pass\n",
    "\n",
    "class Dog(Pet):\n",
    "    pass\n",
    "    \n",
    "class ExportJSON:\n",
    "    pass\n",
    "\n",
    "class ExDog(Dog, ExportJSON):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "609625a7",
   "metadata": {},
   "source": [
    "Давайте представим, что нам понадобится экспортировать данные не только в формате `json`, но и еще в другом формате, например, пусть будет это `XML`. Как тогда изменится структура нашей программы? Тогда, очевидно, нам нужно будет добавить еще один класс-примесь, и тогда наш код будет выглядеть так, как показано на слайде, то есть пока не вдаемся в подробности реализации самих классов-примесей."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "e3465f4b",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExportJSON:\n",
    "    def to_json(self):\n",
    "        pass\n",
    "\n",
    "class ExportXML:\n",
    "    def to_xml(self):\n",
    "        pass\n",
    "    \n",
    "class ExDog(Dog, ExportJSON, ExportXML):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6e5b3966",
   "metadata": {},
   "outputs": [],
   "source": [
    "dog = ExDog(\"Фокс\", \"мопс\")\n",
    "dog.to_xml()\n",
    "dog.to_json()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55fa6dc7",
   "metadata": {},
   "source": [
    "У нас появился наш новый класс-примесь `ExportXML`, у него есть свой собственный метод `to_xml`, и наш итоговый класс `ExDog` теперь наследуется уже от трех классов-родителей. Тем самым объекты этого класса `ExDog` смогут экспортировать данные как в `json`, так и в `xml`.\n",
    "\n",
    "Давайте представим, что нам нужно будет добавлять еще несколько методов для экспорта данных. Какие сложности могут возникнуть в таком случае?\n",
    "\n",
    "Во-первых, нам постоянно придется изменять код нашего класса `ExDog`, постоянно дописывать туда новые классы-примеси.\n",
    "\n",
    "Во-вторых, это уже сильно усложнит сам код, и в итоговой программе нам нужно будет вызывать разные методы этих классов-примесей, то есть мы будем постоянно что-то изменять в своей программе. И это уже не то, чего хотелось бы нам достичь.\n",
    "\n",
    "Давайте попробуем рассмотреть, как в таком случае работает композиция. Для этого нам понадобится Jupyter Notebook. Давайте попробуем решить эту задачу немного другим способом."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f698b300",
   "metadata": {},
   "source": [
    "Предположим, у нас будет класс для экспорта. Давайте объявим его. Пусть он будет называться `PetExport` и у него будет один метод, `export`. Обратите внимание, я сейчас использовал генерацию исключения. Об исключениях мы с вами будем говорить в последующих лекциях, а пока вам нужно для себя усвоить, что мы не будем создавать объекты данного класса `PetExport` и он предназначен только для наследования. Давайте объявим другие классы, которые будут заниматься экспортом данных. `JSON`. Также наш класс `export` должен принимать сам объект, который он будет экспортировать. Пока опустим реализацию самого экспорта. Также добавим класс для экспорта данных в формате `xml`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "b2e18c93",
   "metadata": {},
   "outputs": [],
   "source": [
    "class PetExport:\n",
    "    def export(self):\n",
    "        raise NotImplementedError\n",
    "\n",
    "class ExportJSON:\n",
    "    def export(self, dog):\n",
    "        pass\n",
    "    \n",
    "class ExportXML:\n",
    "    def export(self, dog):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5b69659",
   "metadata": {},
   "source": [
    "Итак, у нас готова иерархия классов для экспорта данных. Давайте теперь вспомним наши классы. \"Питомец\", у которого у каждого питомца было имя, мы его сохраняли в атрибуте `name`, и также класс \"собачка\", у которого появился дополнительный атрибут порода, `breed`, и мы сохранили его в одноименном атрибуте."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "14a2f4cc",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Pet:\n",
    "    def __init__(self, name=None):\n",
    "        self.name = name\n",
    "        \n",
    "        \n",
    "class Dog(Pet):\n",
    "    def __init__(self, name, breed=None):\n",
    "        super().__init__(name)\n",
    "        self.breed = breed"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3b38ead",
   "metadata": {},
   "source": [
    "Давайте теперь объявим класс `ExDog`. Теперь мы не будем использовать свое множественное наследование, мы будем расширять существующий класс `Dog`. Переопределим инициализатор. Он тоже будет принимать на вход имя питомца, породу, а также дополнительный объект для экспорта данных. Вызовем конструктор базового класса и сохраним объект `exporter` в `self`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "3098e00d",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExDog(Dog):\n",
    "    def __init__(self, name, breed=None, exporter=None):\n",
    "        super().__init__(name, breed)\n",
    "        self._exporter = exporter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40256353",
   "metadata": {},
   "source": [
    "Обратите внимание, что в данном классе мы не используем наследование. Вместо этого мы используем композицию и передаем нужный объект для экспорта в инициализаторе этого класса. Давайте добавим ему метод `export`, и теперь наш класс сможет экспортировать данные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "e3cd85ac",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExDog(Dog):\n",
    "    def __init__(self, name, breed=None, exporter=None):\n",
    "        super().__init__(name, breed)\n",
    "        self._exporter = exporter\n",
    "        \n",
    "    def export(self):\n",
    "        return self._exporter.export(self)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5484df82",
   "metadata": {},
   "source": [
    "Делать он это будет при помощи нашего `exporter`'а. Передадим ему `self` для экспорта. \n",
    "\n",
    "Давайте попробуем создать экземпляр нашего класса `ExDog`. Пусть это будет собачка \"Шарик\", порода \"Дворняга\". Предположим, мы хотим, чтобы объект этого класса умел экспортировать свои данные в `xml`. Давайте передадим нужный `exporter`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "0a9757ad",
   "metadata": {},
   "outputs": [],
   "source": [
    "dog = ExDog(\"Шарик\", \"Дворняга\", exporter=ExportXML())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "63846315",
   "metadata": {},
   "source": [
    "Обратите внимание, что при использовании композиции нужный объект создается именно в момент выполнения уже конкретной программы. Давайте попробуем выполнить метод `export`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "d2a062c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "dog.export()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f88aac87",
   "metadata": {},
   "source": [
    "Осталось реализовать только методы для экспорта в начальной иерархии классов. Давайте сделаем это. С `json` все просто, мы уже разбирали пример, используя модуль `json` и метод `dumps`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "cfded06f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "class ExportJSON:\n",
    "    def export(self, dog):\n",
    "        return json.dumps(\n",
    "            {\n",
    "                \"name\": dog.name,\n",
    "                \"breed\": dog.breed\n",
    "            }\n",
    "        )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09aad031",
   "metadata": {},
   "source": [
    "Давайте реализуем теперь метод `export` в классе `ExportXML`. Реализация может выглядеть достаточно просто. Создаем сам `xml`. В нем будут объекты `name` и порода нашей собаки. Всё, осталось отформатировать данную строку при помощи метода `format`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "376dfd98",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExportXML:\n",
    "    def export(self, dog):\n",
    "        return \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
    "<dog>\n",
    "    <name>{name}</name>\n",
    "    <breed>{breed}</breed>\n",
    "</dog>\n",
    "\"\"\".format(name=dog.name, breed=dog.breed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "bcfad312",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<dog>\\n    <name>Шарик</name>\\n    <breed>Дворняга</breed>\\n</dog>\\n'"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dog = ExDog(\"Шарик\", \"Дворняга\", exporter=ExportXML())\n",
    "dog.export()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5be78f28",
   "metadata": {},
   "source": [
    "Точно так же мы с легкостью можем сделать экспорт данных и в формате `json`. Можем создать другую собачку. Пусть это будет \"Тузик\" другой породы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "ef8c4c01",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'{\"name\": \"\\\\u0422\\\\u0443\\\\u0437\\\\u0438\\\\u043a\", \"breed\": \"\\\\u041c\\\\u043e\\\\u043f\\\\u0441\"}'"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dog = ExDog(\"Тузик\", \"Мопс\", exporter=ExportJSON())\n",
    "dog.export()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c52e1bfa",
   "metadata": {},
   "source": [
    "Однако, неудобно, если каждый раз задавать метод для экспорта. Давайте немного изменим наш класс `ExDog` и зададим метод для экспорта по умолчанию. Можно это сделать следующим образом. \n",
    "\n",
    "Давайте выполним еще одну важную вещь. Мы проверим на то, является ли переданный объект экземпляром класса `PetExport` и вообще может ли он выполнять экспорт данных. Для этого мы можем воспользоваться проверкой `isinstance`. Нам нужен наш `exporter` и указываем класс. Что делать, если нам передали объект, который не может выполнять экспорт? Давайте пока сгенерируем исключение.\n",
    "Для вас это будет означать, что программа дальше не сможет продолжить свою работу и будет остановлена. Например, `ValueError` нам подойдет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "624c763b",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExDog(Dog):\n",
    "    def __init__(self, name, breed=None, exporter=None):\n",
    "        super().__init__(name, breed)\n",
    "        \n",
    "        self._exporter = exporter or ExportJSON()\n",
    "        \n",
    "        if not isinstance(self._exporter, PetExport):\n",
    "            raise ValueError(\"bad exporter\", exporter)\n",
    "        \n",
    "    def export(self):\n",
    "        return self._exporter.export(self)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "812aff80",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ExportJSON(PetExport):\n",
    "    def export(self, dog):\n",
    "        return json.dumps(\n",
    "            {\n",
    "                \"name\": dog.name,\n",
    "                \"breed\": dog.breed\n",
    "            }\n",
    "        )\n",
    "\n",
    "class ExportXML(PetExport):\n",
    "    def export(self, dog):\n",
    "        return \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
    "<dog>\n",
    "    <name>{name}</name>\n",
    "    <breed>{breed}</breed>\n",
    "</dog>\n",
    "\"\"\".format(name=dog.name, breed=dog.breed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "id": "41174209",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'{\"name\": \"\\\\u0422\\\\u0443\\\\u0437\\\\u0438\\\\u043a\", \"breed\": \"\\\\u041c\\\\u043e\\\\u043f\\\\u0441\"}'"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dog = ExDog(\"Тузик\", \"Мопс\")\n",
    "dog.export()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe1efbd1",
   "metadata": {},
   "source": [
    "Теперь, если мы не объявим объект `exporter`, по умолчанию наша собачка будет экспортировать данные в `json`. Давайте еще раз посмотрим на то, какой код у нас получился, и подумаем, что с ним произойдет, если его нужно будет расширить. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "4bb4fda8",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "class Pet:\n",
    "    def __init__(self, name=None):\n",
    "        self.name = name\n",
    "        \n",
    "        \n",
    "class Dog(Pet):\n",
    "    def __init__(self, name, breed=None):\n",
    "        super().__init__(name)\n",
    "        self.breed = breed\n",
    "        \n",
    "        \n",
    "class PetExport:\n",
    "    def export(self):\n",
    "        raise NotImplementedError\n",
    "\n",
    "        \n",
    "class ExportJSON(PetExport):\n",
    "    def export(self, dog):\n",
    "        return json.dumps(\n",
    "            {\n",
    "                \"name\": dog.name,\n",
    "                \"breed\": dog.breed\n",
    "            }\n",
    "        )\n",
    "\n",
    "    \n",
    "class ExportXML(PetExport):\n",
    "    def export(self, dog):\n",
    "        return \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
    "<dog>\n",
    "    <name>{name}</name>\n",
    "    <breed>{breed}</breed>\n",
    "</dog>\n",
    "\"\"\".format(name=dog.name, breed=dog.breed)\n",
    "    \n",
    "    \n",
    "class ExDog(Dog):\n",
    "    def __init__(self, name, breed=None, exporter=None):\n",
    "        super().__init__(name, breed)\n",
    "        \n",
    "        self._exporter = exporter or ExportJSON()\n",
    "        \n",
    "        if not isinstance(self._exporter, PetExport):\n",
    "            raise ValueError(\"bad exporter\", exporter)\n",
    "        \n",
    "    def export(self):\n",
    "        return self._exporter.export(self)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "291bee59",
   "metadata": {},
   "source": [
    "Предположим, если нам нужно будет добавить в этот код новый метод для экспорта, мы с легкостью сможем сделать это. Просто объявим новый класс, добавим его в существующую иерархию для экспорта, а класс `ExDog` теперь мы больше менять не будем. Таким образом, если у нас система будет усложняться, наш класс `ExDog` будет оставаться неизменяемым или неизменным. А экспортировать в различные форматы мы уже сможем легко и удобно в итоговой программе, подставив нужный exporter или создав его.\n",
    "\n",
    "Не пугайтесь, если вам сейчас непонятны плюсы данного подхода по отношению к наследованию. Чем вы больше будете программировать на Python, пробовать и реализовывать различные подходы, тем вам станет понятно, где действительно лучше применять наследование, а в каких местах следует использовать композицию."
   ]
  }
 ],
 "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.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
