diff --git a/Py8.ipynb b/Py8.ipynb
index 43a7f70..9e07402 100644
--- a/Py8.ipynb
+++ b/Py8.ipynb
@@ -17,18 +17,32 @@
"\n",
"> Python es un lenguaje de programación orientado a objetos (**OOP**, del inglés *Object-Oriented Programming*), como también lo son C++, Java, C#, Swift, JavaScript y otros populares. La comprensión del paradigma permite manipular las librerías utilizadas y diseñar algoritmos de una mejor manera. Este documento contiene una explicación de conceptos básicos junto con la creación de ejemplos de clases y objetos en Python, además de algunos principios de diseño de software.\n",
"\n",
- "*Fabián Abarca Calderón*\n",
+ "*Fabián Abarca Calderón*
\n",
+ "*Mario R. Peralta A.*\n",
"\n",
- "---"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Fundamentos de OOP\n",
+ "---\n",
"\n",
- "#### Notación del punto\n",
+ "## Tabla de contenido\n",
+ "\n",
+ "1. [Importancia de diseño](#importancia-dise)\n",
+ "1. [Fundamentos de POO](#fundamentos-poo)\n",
+ "1. [Atributos y Métodos especiales](#special-meth)\n",
+ "1. [Clase de datos](#clase-datos)\n",
+ "1. [Pilares de POO](#pilares-poo)\n",
+ "1. [Principios de diseño de software](#dise-software)\n",
+ "\n",
+ "## Introducción: Importancia del diseño \n",
+ "\n",
+ "*El programa es el pentágrama, codificar la guitarra.* Para advertir las aguas que vamos a navegar convengamos que codificar no es programar como se suele creer. “Programa” nos viene del griego “pro” (previo) y “gramma” (resultado de escribir), se usaba para referirse a la orden del día, o sea, las actividades planeadas y prescritas que servían como guía durante funciones organizadas [[1](https://etimologias.dechile.net/?programa)].\n",
+ "La composición artística, un diseño a fin de cuenta, yace en una estructura estricta estándar. A la hora de comunicar con claridad y rigor una pulsión musical sin importar cuan inasible o inefable sea, el pentágrama no falla, aunque arcaico, es universal e inequívoco; de modo tal que una orquesta, a pesar del amplio espectro de instrumentos, logra convenir y coordinar la melodía y armonía que el pentágrama dicta. La guitarra, sin embargo, es sólo un *mecanismo* y como tal o se moderniza o se resigna a la obsolescencia, lo mismo que un lenguaje de programación (código) en la medida en que la inteligencia artificial se abre paso incesante, no así la Programación, que es incluso anterior a las computadoras. Programar no está sujeto a codificar, el uno es un proceso creativo y el otro mecánico, como el pentágrama insensible del instrumento. El programa marca la pauta, debe ser inequívoco, estricto y sin ambigüedades; en esencia no dista mucho de una receta de cocina, pero un diagrama *UML* (Unified Modeling Language) debería ser suficiente, así cualquier desarrollador (guitarrista) se ocupa de codificarlo para que la máquina lo entienda. El programa es el sustento de un algoritmo que aspira a resolver una familia de problemas salvo que no basta resolverlo, sino además -en un mundo de recursos finitos- hacerlo de manera óptima y acaso estética puesto que «bonito es mejor que feo» he aquí la necesidad de diseñar, el Diseño es el que impone el flujo lógico para tal fin. Como la casa no se empieza por el tejado, el diseño no es necesario, es inevitable. Éste documento es un esfuerzo por desempacar los principios del diseño de software a saber: \"**Apuntar a lo más simple que pueda funcionar**\", luego \"**No lo vas a necesitar**\" y finalmente \"**Una y sólo una vez**\".\n",
+ "\n",
+ "Dicho lo cual, encierra algo de verdad aquello que dijo Shakespear, no el William, el otro, Ronald Shakespear:\n",
+ "\n",
+ "> *«El diseño no salvará el mundo, pero el mundo no se salvará si no se diseña».*\n",
+ "\n",
+ "## 1. Fundamentos de POO \n",
+ "\n",
+ "### 1.1. Notación del punto\n",
"\n",
"Ejemplos:\n",
"\n",
@@ -37,9 +51,24 @@
"- `.atr` es un **atributo** de una clase u objeto\n",
"- `.met()` es un **método** de una clase u objeto\n",
"\n",
- "Ejemplo:\n",
- "\n",
- "```python\n",
+ "A saber:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "La función da: 625\n",
+ "El método da: 625\n"
+ ]
+ }
+ ],
+ "source": [
"# Definición de una variable\n",
"var = 25\n",
"\n",
@@ -48,7 +77,7 @@
" return num**2\n",
"\n",
"# Definición de una clase\n",
- "class Ope:\n",
+ "class Ope():\n",
" # Definición de un atributo en una clase\n",
" def __init__(self, atr):\n",
" self.atr = atr\n",
@@ -58,70 +87,1713 @@
" return self.atr**2\n",
"\n",
"# Ejecución de la función\n",
- "print(f'La función da: {op(num)}')\n",
+ "print(f'La función da: {fun(var)}')\n",
"\n",
"# Creación de un objeto con el atributo \"num\"\n",
- "obj = Ope(num)\n",
- "print(f'El método da: {obj.met()}')\n",
- "```\n",
+ "obj = Ope(var)\n",
+ "print(f'El método da: {obj.met()}')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "*Nota*: Python o JavaScript tienen un sistema de *tipado dinámico* (*dynamically typed*), esto quiere decir que el *tipo* de variable (entero, flotante, literal...) no es explícitamente declarado sino que es comprobado durante la ejecución, en el caso de Python además es *tipado fuerte* o sea que el tipo no cambia automáticamente salvo que sea muy obvio como operar un `entero` con un `flotante` de otro modo salta la excepción `TypeError` e.g.\n",
+ "\n",
+ "El dinámico:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Los tipos son: ['int', 'float', 'str', 'list']\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Tipado dinámico.\"\"\"\n",
+ "\n",
+ "\n",
+ "def show_type(*args):\n",
+ " \"\"\"Retornar tipo.\"\"\"\n",
+ " return [a.__class__.__name__ for a in args]\n",
"\n",
- "*Nota*: Python tiene un \"sistema de tipos\" dinámico (*dynamically typed*), esto quiere decir que el *tipo* de variable (entero, flotante, literal...) no es explícitamente declarado sino que es comprobado durante la ejecución. Por ejemplo:\n",
"\n",
- "```python\n",
"# Un número entero\n",
"var1 = 3\n",
- "\n",
"# Un número flotante\n",
- "var2 = 3.0\n",
- "\n",
+ "var2 = 3.0 + var1\n",
"# Una cadena de caracteres con el símbolo 3\n",
"var3 = \"3\"\n",
- "\n",
"# Una lista con el valor 3\n",
"var4 = [3]\n",
+ "\n",
+ "print(f\"Los tipos son: {show_type(var1, var2, var3, var4)}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "*Nota*: Más adelante en [Atributos y Métodos especiales](#special-meth) se revisa la notación que encierra el atributo con doble guión bajo `.__name__`.\n",
+ "\n",
+ "El tipado fuerte:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "TypeError: unsupported operand type(s) for +: 'int' and 'str'\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Tipado fuerte: TypeError.\"\"\"\n",
+ "\n",
+ "var5 = 7 # Entero\n",
+ "var6 = \"7\" # Cadena\n",
+ "\n",
+ "try:\n",
+ " var7 = var5 + var6\n",
+ " print(f\"El resultado es: {var7}\")\n",
+ "except TypeError as e:\n",
+ " print(f\"TypeError: {e}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A continuación, se utiliza la función incorporada `eval()` para evaluar la expresión y obtener el resultado previsto para la operación actual, útil cuando los datos provienen en formato de intercambio de datos e.g. `.json`, cuyas llaves, en el objeto de python resultante, se convierten a \"str\" sin embargo son requeridos como \"tuple\"."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Tipos de datos antes: {'str'}\n",
+ "Tipos de datos después: {'tuple'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Coordenadas de universidades.\n",
+ "\n",
+ "Note que las llaves son las coordenas (x, y)\n",
+ "y vienen como cadenas, por tanto su indexación\n",
+ "no es viable. La función ``eval()`` lo resuelve.\n",
+ "\"\"\"\n",
+ "\n",
+ "import json\n",
+ "data = '{\"(40, -74.06)\": \"UCR\", \"(34.5, -118.2)\": \"UNA\", \"(41, -87.6)\": \"TEC\"}'\n",
+ "old_data = json.loads(data)\n",
+ "new_data = {eval(coord): uni for coord, uni in old_data.items()}\n",
+ "old_keys_type = {type(position).__name__ for position in old_data}\n",
+ "new_keys_type = {type(position).__name__ for position in new_data}\n",
+ "\n",
+ "print(\"Tipos de datos antes: \", old_keys_type)\n",
+ "print(\"Tipos de datos después: \", new_keys_type)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note además que al ser tupla se pude indexar y no sólo eso si se verifica el tipo de dato será flotante o entero, o sea, `eval()` convirtió la cadena a tupla y el contenido de la tupla al tipo de dato más adecuado de manera dinámica.\n",
+ "\n",
+ "Aunque python sea tipado dinámico se reconoce la buena práctica de asignar de ante mano una \"pista\" del tipo de variable ver [sintaxis para anotación de variables](https://peps.python.org/pep-0526/) para etiquetar con diligencia los tipos de clases o variables a fin de un código legible y cómodo de trasegar, ¿No es cierto aquello de que «el código es más leído que escrito»? En la sección [Clase de Datos](#clase-datos) se explotan las virtudes de ésta práctica con más ahínco.\n",
+ "\n",
+ "### 1.2. Atributos y Métodos Especiales \n",
+ "\n",
+ "Éstas dos entidades se caracterizan por tener, al inicio y al final, doble guión bajo (**d**uble **under**scores) por eso tienen el alias, no desatinado, \"dunder\". Algo las une inevitables y es que subyacen a la estructura (no están a la vista) y aún así Python les llama automáticamente en respuesta \"predeterminada\" a operaciones específicas. Para consulta de éstos se ancla la documentación disponible en la página oficial de python: [Atributos especiales](https://docs.python.org/3/library/stdtypes.html#special-attributes) y [Métodos especiales](https://docs.python.org/3/reference/datamodel.html#special-method-names). A pase de tanteo, se muestran algunas en tablas:\n",
+ "\n",
+ "Atributos especiales: Para efectos de introspección.\n",
+ "\n",
+ "| Atributo | Descripción |\n",
+ "| :--- | ---: |\n",
+ "| `__dict__` | Ver campos de una instancia |\n",
+ "| `__name__` | Nombre del tipo de la clase |\n",
+ "| `__annotations__` | Pistas de tipos |\n",
+ "| `__doc__` | Documentación de la entidad |\n",
+ "\n",
+ "\n",
+ "Métodos especiales: Para efectos de personalización.\n",
+ "\n",
+ "| Método | Sintaxis | Operación |\n",
+ "| :--- | :----: | ---: |\n",
+ "| `.__add__()` | + | Sumar |\n",
+ "| `.__lt__()` | < | Menor que |\n",
+ "| `.__and__()` | & | Intersección |\n",
+ "| `.__iadd__()` | += | Aumentador |\n",
+ "\n",
+ "Los de un objeto particular se pueden hacer revelar si se invoca la función `dir()` que por debajo llama a `.__dir__()` así:\n",
+ "\n",
+ "```Python\n",
+ ">>> from datetime import date\n",
+ ">>> day_today = date.today()\n",
+ ">>> day_today.__dir__()\n",
+ "['__new__', '__str__', '__getattribute__', '__lt__', ..., '__class__']\n",
"```\n",
"\n",
- "## Conceptos de programación orientada a objetos\n",
+ "Pero, si la camisa de fuerza aprieta, no hace falta que sea una respuesta predeterminada también se puede *recargar el operador* (los métodos) con objeto de crear una clase a la medida, por ejemplo, a continuación se recargan: `.__str__()` para mostrar los resultados más amigable al usuario (usar `.__repr__()` para mostrar al desarrollador debe ser exhaustivo en los atributos desplegados), también `.__len__()` el cual considera que el tamaño de un aula no és el área sino el tamaño del grupo o sea, número de estudiantes, seguidamente se recarga `.__eq__()` que contempla, para empezar, si los tipos de clases a comparar son los mismos, luego estable que para que dos `ClassRoom` sean iguales deben tener mismo horario y estudiantes (no necesariamente número identificador de la puerta); finalmente se recarga `.__add__()` que entiende una suma de aulas como una mezcla de los grupos esto és: Crea y devuelve una nueva instancia que suma el número de sillas, área, unión de estudiantes y otros."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Aula=73, horario=2024-03-17, materia=Mathematics\n",
+ "Número de alumnos en grupo de sociales: 5\n",
+ "Comparar si grupos son iguales: False\n",
+ "Juntar grupos: Aula=100, horario=2024-03-18, materia=\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Aula de clases con operaciones personalizadas.\"\"\"\n",
+ "import datetime\n",
+ "\n",
+ "\n",
+ "class ClassRoom():\n",
+ " def __init__(\n",
+ " self, chairs: int, students: list[str],\n",
+ " area: float, unit: str, schedule: datetime.date,\n",
+ " subject: str, door_num: int\n",
+ " ) -> None:\n",
+ " self.chairs = chairs\n",
+ " self.students = students\n",
+ " self.area = area\n",
+ " self.unit = unit\n",
+ " self.schedule = schedule\n",
+ " self.subject = subject\n",
+ " self.door_num = door_num\n",
+ "\n",
+ " def __str__(self) -> str:\n",
+ " num_id = self.door_num\n",
+ " day = self.schedule\n",
+ " subject = self.subject\n",
+ " str_out = f\"Aula={num_id}, horario={day}, materia={subject}\"\n",
+ " return str_out\n",
+ "\n",
+ " def __len__(self) -> int:\n",
+ " return len(self.students)\n",
+ "\n",
+ " def __eq__(self, other: object) -> bool:\n",
+ " if not isinstance(other, type(self)):\n",
+ " raise TypeError(\"Tipos de clase diferentes.\")\n",
+ " day_i = self.schedule\n",
+ " day_j = other.schedule\n",
+ " group_i = self.students\n",
+ " group_j = other.students\n",
+ " return all([day_i == day_j, group_i == group_j])\n",
+ "\n",
+ " def __add__(self, other: object):\n",
+ " if not isinstance(other, type(self)):\n",
+ " raise TypeError(\"Operador + no soportado para tipos diferentes.\")\n",
+ " elif not self.unit == other.unit:\n",
+ " raise TypeError(\"Unidades no compatibles.\")\n",
+ " chairs = self.chairs + other.chairs\n",
+ " students = self.students + other.students\n",
+ " area = self.area + other.area\n",
+ " unit = self.unit\n",
+ " tomorrow = self.schedule + datetime.timedelta(days=1)\n",
+ " subject = f\"<{self.subject} & {other.subject}>\"\n",
+ " door_num = self.door_num + other.door_num\n",
+ " return type(self)(\n",
+ " chairs, students, area, unit, tomorrow, subject, door_num\n",
+ " )\n",
"\n",
- "### Abstracción\n",
"\n",
- "### Encapsulación\n",
+ "def run_example():\n",
+ " math_studs = [\"Nemo\", \"Milo\", \"Foo\"]\n",
+ " social_studs = [\"Egg\", \"Bone\", \"Beef\", \"Troya\", \"Nero\"]\n",
+ " math_group = ClassRoom(chairs=5,\n",
+ " students=math_studs,\n",
+ " area=22.2,\n",
+ " unit=\"m2\",\n",
+ " schedule=datetime.date.today(),\n",
+ " subject=\"Mathematics\",\n",
+ " door_num=73)\n",
"\n",
- "### Herencia\n",
+ " social_group = ClassRoom(chairs=23,\n",
+ " students=social_studs,\n",
+ " area=41.1,\n",
+ " unit=\"m2\",\n",
+ " schedule=datetime.date.today(),\n",
+ " subject=\"Social Science\",\n",
+ " door_num=27)\n",
"\n",
- "### Polimorfismo\n",
- "\n"
+ " print(math_group)\n",
+ " print(\"Número de alumnos en grupo de sociales: \", len(social_group))\n",
+ " print(\"Comparar si grupos son iguales: \", math_group == social_group)\n",
+ " print(\"Juntar grupos: \", math_group + social_group)\n",
+ "\n",
+ "\n",
+ "if __name__ == \"__main__\":\n",
+ " run_example()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
+ "### 1.3. Clase de Datos \n",
+ "\n",
+ "Para crear una clase en lugar de una sintaxis redundante como la usada en el ejemplo anterior:\n",
+ "\n",
+ "```Python\n",
+ "class ClassRoom():\n",
+ " def __init__(\n",
+ " self, chairs: int, students: list[str],\n",
+ " area: float, unit: str, schedule: datetime.date,\n",
+ " subject: str, door_num: int\n",
+ " ) -> None:\n",
+ " self.chairs = chairs\n",
+ " self.students = students\n",
+ " self.area = area\n",
+ " self.unit = unit\n",
+ " self.schedule = schedule\n",
+ " self.subject = subject\n",
+ " self.door_num = door_num\n",
+ "```\n",
+ "\n",
+ "És más limpia, intuitiva, directa y menos propensa a errores, la siguiente notación en la que sólo hay que proveer los campos (atributos) y un indicio (pista) de su tipo:\n",
+ "\n",
+ "```Python\n",
+ "@dataclass()\n",
+ "class ClassRoom():\n",
+ " chairs: int\n",
+ " students: list[str]\n",
+ " area: float\n",
+ " unit: str\n",
+ " schedule: datetime.date\n",
+ " subject: str\n",
+ " door_num: int\n",
+ "```\n",
+ "\n",
+ "Con éste artilugio las instancias ya vienen con los dunder `.__repr__()` y `.__eq__()` (y otros) recargados (si la clase ya define `.__eq__()`, este parámetro se ignora), en el caso de éste último, detrás de escena en el método `.__eq__()` compara la clase como si fuera una tupla de sus campos, en orden. Lo que implica que recién salidos del horno ya se pueden mostrar de manera amigable y comparar entre sí.\n",
+ "\n",
+ "En el ejemplo abajo, se crea una instancia de una biblioteca según la universidad que es indicada por medio de un `@classmethod` que cuenta con una base de datos interna. La pleca `|` en una \"anotación de tipo\" significa \"unión\", es decir el tipo de dato de coordenada puede ser ya sea entero o flotante. Note que se carga la función `field()` para varios propósitos, uno de ellos es esconder un atributo, en éste caso las coordenadas ($x, y, z$) sin embargo, en general, el método especial `.__repr__()`, debería mostrar todos los campos a favor de despulgar el código. Otro propósito de `field()` en éste contexto es evitar asignar valores mutable como predeterminados, aunque el objeto `datetime.date` ya es inmutable, por el bien del ejemplo, de todos modos se inicializa el campo con la fecha de hoy. En los resultados se evidencia como la instancia `lib1` cuando se muestra está en formato amigable (sin coordenadas que se vea reflejadas como se indicó antes), no sólo eso, `lib2` y `lib3` son diferentes mientras que `lib1` y `lib3` iguales. Todo ésto sin haber cargado algún dunder explícitamente."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Library(lib_name='Alejandría', country='Masedonia', creation_date=datetime.date(2024, 3, 17))\n",
+ "Comparar si MIT es la misma que Alejadría: False\n",
+ "Comparar si las de Alejadnría son las mismas: True\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Bibliotecas de universidades.\"\"\"\n",
+ "\n",
+ "import datetime\n",
+ "from dataclasses import dataclass, field\n",
"\n",
- "## Principios de diseño de software\n",
"\n",
- "Para: escalabilidad, trazabilidad, mantenimiento, flexibilidad... una \"reflexión\"\n",
+ "def date_today() -> datetime.date:\n",
+ " \"\"\"Fijar fecha de hoy como la predetermina.\"\"\"\n",
+ " return datetime.date.today()\n",
"\n",
- "Siempre un ejemplo ejecutable y, opcionalmente, un ejercicio para aplicar el principio respectivo.\n",
"\n",
- "### Principios SOLID\n",
+ "@dataclass()\n",
+ "class Library():\n",
+ " lib_name: str\n",
+ " coord_x: int | float = field(repr=False)\n",
+ " coord_y: int | float = field(repr=False)\n",
+ " coord_z: int | float = field(repr=False)\n",
+ " country: str\n",
+ " creation_date: datetime.date | None = field(default_factory=date_today)\n",
"\n",
- "#### Principio de responsabilidad única (*Single-Responsibility Principle*, **SRP**)\n",
- "#### Principio de abierto/cerrado (*Open-Closed Principle*, **OCP**)\n",
- "#### Principio de sustitución de Liskov (*Liskov Substitution Principle*, **LSP**)\n",
- "#### Principio de segregación de la interfaz (*Interface Segregation Principle*, **ISP**)\n",
- "#### Principio de inversión de la dependencia (*Dependency Inversion Principle*, **DIP**)\n",
+ " @classmethod\n",
+ " def explore_library(cls, college: str = \"Alejandría\"):\n",
+ " data_base = {\n",
+ " \"Alejandría\": (77.7, 73, 73.73, \"Masedonia\"),\n",
+ " \"Imperial College London\": (12.1, 11, 22.2, \"United Kingdom\"),\n",
+ " \"Massachusetts Institute of Technology\": (\n",
+ " 77.3, 10.3, 22.3, \"United States\"\n",
+ " ),\n",
+ " \"University of Cambridge\": (12.23, 78, 23.6, \"United Kingdom\"),\n",
+ " \"University of Oxford\": (56.4, 63.2, 76, \"United Kingdom\"),\n",
+ " \"Harvard University\": (9.01, 43.1, 12.222, \"United States\")\n",
+ " }\n",
+ " return cls(college, *data_base[college])\n",
"\n",
- "### Nociones de los patrones de diseño"
+ "\n",
+ "def run_example() -> None:\n",
+ " lib1 = Library.explore_library()\n",
+ " lib2 = Library.explore_library(\"Massachusetts Institute of Technology\")\n",
+ " lib3 = Library.explore_library(\"Alejandría\")\n",
+ " print(lib1)\n",
+ " print(\"Comparar si MIT es la misma que Alejadría: \", lib2 == lib3)\n",
+ " print(\"Comparar si las de Alejadnría son las mismas: \", lib1 == lib3)\n",
+ "\n",
+ "\n",
+ "if __name__ == \"__main__\":\n",
+ " run_example()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "---\n",
- "### Más información\n",
+ "## 2. Pilares de POO \n",
+ "\n",
+ "### 2.1. Abstracción\n",
+ "\n",
+ "Para modelar relaciones e interacciones en un sistema los Grafos suelen ser un artificio matemático útil puesto que sólo constan de Vértices y Aristas que los conectan para al final describir una red. Así para representar las relaciones e interacciones entre entes basta un grafo que represente, por ejemplo, la \"Asociación\" Docente (D) y Estudiante (E) que son \"Agregados\" de una Universidad (U) \"Compuesta de\" la facultad de ingeniería (I) la cual provee becas a estudiantes y salarios a estudiantes y docentes.\n",
+ "\n",
+ "Una Universidad (U) \"compuesta de\" la facultad de ingeniería (I) con \"agregados\" (menos estricto que *composición*) tales como Docentes (D) o Estudiantes (E) que \"dependen de\" un salario y una beca respectivamente, y que se \"asocian\" , éstos dos últimos, en un aula de clases (ver figura).\n",
+ "Las relaciones: Asociación, Dependencia, Composición y Agregación; se verán más adelante en [Herencia](#3-herencia).\n",
+ "\n",
+ "```mermaid\n",
+ "graph LR\n",
+ "C((U)) --- |Personal| A\n",
+ "C((U)) --- |Facultad| F\n",
+ "F((I)) --> |Beca| B\n",
+ "F((I)) --> |Salario| A\n",
+ "C --- |Matrícula| B\n",
+ "A((D)) --- |Aula| B((E))\n",
+ "```\n",
+ "\n",
+ "Es evidente que para efectos de establecer una relación tenemos licencia para prescindir del proceso de matrícula, edificios, hojas de vida, nombres, fechas de nacimiento, padres y cualquier otro tipo de información biográfica irrelevante para tal fin. Ésto es un ejemplo de *Abstracción* la cual puede definirse como: \"La habilidad para modelar un objeto, sistema o fenómeno por sus elementos fundamentales en un contexto dado tal que se permita prescindir de aspectos irrelevantes\", aunque nunca mejor dicho que Borges en su cuento *Funez el Memorioso* «Pensar es olvidar diferencias, es generalizar, abstraer». En otras palabras: *Lo que no suma, resta.*\n",
+ "\n",
+ "En ese sentido, los objetos se limitan a modelar las cualidades y comportamientos de los objetos reales en un contexto dado, o sea, una clase `University` podría tener lugar ya sea en un optimizador de consumo energético por sector o bien en una plataforma de programas de beca. En un caso el modelo contendría estados (atributos) y comportamientos (métodos) que tiene que ver con el consumo de energía útil por cada aparato (véase [Ejemplo 1](#ej1)), mientras que en el otro con fondos y requisitos de orden académicos (véase [Ejemplo 2](#ej2)).\n",
+ "\n",
+ "#### Ejemplo 1 "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "El consumo de energía útil es: 114.0PJ\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Optimizador de consumo energético en sector académico.\n",
+ "\n",
+ "Determinar la demanda de energía útil en petajoule (PJ)\n",
+ "de la Univesidad al fondo del mar llamada Atlantis\n",
+ "la cual demanda 120PJ por año.\n",
+ "\"\"\"\n",
+ "\n",
+ "\n",
+ "class University():\n",
+ " \"\"\"Matriz energética.\n",
+ "\n",
+ " Atributos\n",
+ " ---------\n",
+ " energy_PJ : float\n",
+ " Demanda de energía en PJ.\n",
+ " losses_PJ : float\n",
+ " Pértidas en PJ. El 5% de demanda.\n",
+ "\n",
+ " Métodos\n",
+ " -------\n",
+ " useful_energy()\n",
+ " Retorna el consumo después de pédidas\n",
+ " en PJ.\n",
+ " \"\"\"\n",
+ "\n",
+ " def __init__(self, energy: float):\n",
+ " \"\"\"Construir.\"\"\"\n",
+ " self.energy_PJ = energy\n",
+ "\n",
+ " @property\n",
+ " def losses_PJ(self):\n",
+ " \"\"\"Calcular pédidas.\"\"\"\n",
+ " return self.energy_PJ*0.05\n",
+ "\n",
+ " def useful_energy(self) -> float:\n",
+ " \"\"\"Calcular consumo útil.\"\"\"\n",
+ " return self.energy_PJ - self.losses_PJ\n",
+ "\n",
+ "\n",
+ "def run_example() -> None:\n",
+ " # Universidad de Atlantis con demanda de 120PJ\n",
+ " atlantis = University(energy=120)\n",
+ " useful_demand = atlantis.useful_energy()\n",
+ " print(f\"El consumo de energía útil es: {useful_demand}PJ\")\n",
+ "\n",
+ "\n",
+ "if __name__ == \"__main__\":\n",
+ " run_example()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "*Nota*: El **decorador** `@property` es útil para generar dinámicamente estados que dependen de otros de sus propios campos (atributos). Revisar sección [Polimorfismo](#4-polimorfismo) para ahondar en un diseño maleable.\n",
+ "\n",
+ "### 2.2. Encapsulamiento \n",
+ "\n",
+ "El Encapsulamiento es la habilidad de privar los estados y comportamientos de interactuar con el resto del programa en un esfuerzo por prevenir acciones inválidas o ilógicas. Lo que resulta en un andamiaje que subyace a la interfaz la cual deja ver sólo lo necesario y suficiente.\n",
+ "\n",
+ "En el ejemplo a continuación se detectan atributos y métodos con un `__` (doble guión bajo), sólo al comienzo (por sintaxis) ésto les concede un carácter exclusivo a su propia clase `University` de tal manera que sólo son accesibles dentro de dicha clase. Una vez que el usuario proporciona la información, lo primero es que no tiene permitido modificar los requisitos, pues ya son los que son, de ahí el atributo `__requirements`, enseguida lo único que le corresponde es proveer lo solicitado y conocer los resultados lo demás, leer y validar, es asunto de la institución (instancia) `atlantis` por lo tanto `__read_data()` y `__validate()` son de acceso restringido.\n",
+ "\n",
+ "Note además que el algoritmo antes de proceder con una evaluación de los criterios **Referencias** y **Grado** advierte la posibilidad de que los campos **Nombre** o **ID** estén vacíos, en cuyo caso salta un mensaje y declina la solicitud, dado que tal medida preventiva no se hace explícita luce una cualidad típica del encapsulamiento.\n",
+ "\n",
+ "#### Ejemplo 2 \n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "¡Felicidades!\n",
+ "El aplicante aplica al programa de becas.\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Plataforma de Becas.\n",
+ "\n",
+ "Plataforma que determina si Nemo aplica para beca\n",
+ "en la Universidad de Atlantis al fondo del mar.\n",
+ "Requisitos:\n",
+ "\n",
+ " - Formulario: \"Nombre\", \"ID\"\n",
+ " - Tres cartas de referencia: \"Referencias\"\n",
+ " - Títulos Académicos: \"Grado\"\n",
+ "\n",
+ "Nota: En caso de faltar alguno el aplicante\n",
+ "Nemo no aplica a programa de becas.\n",
+ "\"\"\"\n",
+ "\n",
+ "\n",
+ "class University():\n",
+ " \"\"\"Plataforma de becas Atlantis.\n",
+ "\n",
+ " Atributos\n",
+ " ---------\n",
+ " data : dict\n",
+ " Documentos e información personal del\n",
+ " aplicante.\n",
+ " __requirements : list\n",
+ " Atributo privado. Criterios a evaluar.\n",
+ "\n",
+ " Métodos\n",
+ " -------\n",
+ " __read_data()\n",
+ " Método privado. Leer y asignar valores de la estructura\n",
+ " de datos.\n",
+ " __validate()\n",
+ " Método privado. Asigna booleano en caso de cumplir o no.\n",
+ " results()\n",
+ " ``False`` si no aplica y ``True`` de otro modo.\n",
+ " \"\"\"\n",
+ "\n",
+ " def __init__(self, data: dict):\n",
+ " \"\"\"Construir institución de educación superior.\"\"\"\n",
+ " self.data = data\n",
+ " self.__requirements = {\n",
+ " \"Form\": [\"Name\", \"ID\"],\n",
+ " \"References\": 3,\n",
+ " \"Degree\": {\"Bachelor\", \"Master\"}\n",
+ " }\n",
+ "\n",
+ " def __read_data(self) -> tuple:\n",
+ " \"\"\"Leer datos.\"\"\"\n",
+ " # Tomar datos del usuario\n",
+ " info = self.data\n",
+ " name = info[\"Nombre\"]\n",
+ " idnum = info[\"ID\"]\n",
+ " ref = len(info[\"Referencias\"])\n",
+ " title = info[\"Grado\"]\n",
+ " return (\n",
+ " name,\n",
+ " idnum,\n",
+ " ref,\n",
+ " title\n",
+ " )\n",
+ "\n",
+ " def __validate(self) -> bool:\n",
+ " \"\"\"Evaluar contra criterios.\"\"\"\n",
+ " name, idnum, ref, title = self.__read_data()\n",
+ "\n",
+ " # Llamar atributos\n",
+ " n_ref = self.__requirements[\"References\"]\n",
+ " degrees = self.__requirements[\"Degree\"]\n",
+ " # Comparar datos\n",
+ " bool_ref = ref >= n_ref\n",
+ " bool_title = title in degrees\n",
+ " # Asignar booleano\n",
+ " if name and idnum:\n",
+ " return all([bool_ref, bool_title])\n",
+ " print(\"MissingData: Formulario incompleto.\")\n",
+ " return False\n",
+ "\n",
+ " def results(self) -> bool:\n",
+ " \"\"\"Mostrar resultados.\"\"\"\n",
+ " bool_result = self.__validate()\n",
+ " if bool_result:\n",
+ " print(f\"¡Felicidades!\")\n",
+ " return True\n",
+ "\n",
+ " print(\"Por favor completar documentación.\")\n",
+ " return False\n",
+ "\n",
+ "\n",
+ "def run_example() -> None:\n",
+ " # Datos aplicante: Nemo Fuu\n",
+ " nemo = {\n",
+ " \"Nombre\": \"Nemo Fuu\",\n",
+ " \"ID\": 2357111317,\n",
+ " \"Referencias\": [\"Leviatán.pdf\",\n",
+ " \"Neptuno.pdf\",\n",
+ " \"Sirena.pdf\"],\n",
+ " \"Grado\": \"Master\"\n",
+ " }\n",
+ "\n",
+ " # Plataforma de Atlantis\n",
+ " atlantis = University(data=nemo)\n",
+ " if atlantis.results():\n",
+ " print(f\"El aplicante <{nemo['Nombre']}> aplica al programa de becas.\")\n",
+ " else:\n",
+ " print(\"Los sentimos, no cumple requisitos.\")\n",
+ "\n",
+ "\n",
+ "if __name__ == \"__main__\":\n",
+ " run_example()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.3. Herencia \n",
+ "\n",
+ "Otra forma de encapsular es por medio de una *Interfaz* equivalente a una *clase abstracta* en el dominio pitónico, aunque en `python` no es obligatorio, se reconoce la buena práctica, puesto que la interfaz permite \"delegar\" la interacción entre objetos a través de los llamados *Métodos Abstractos* de hecho, es el motivo por el que a las interfaces sólo les atañe el comportamiento (métodos) de los objetos y no les es lícito declarar un campo (estado, atributo o propiedad) ni implementar dichos métodos, los llamados: *Métodos Abstractos*, además, deben ser reescritos (implementados) en la *Clase Concreta* la cual **depende** de la interfaz. La idea es parecida, y hasta su sintaxis, cuando se trata de *Herencia*: Relacionar lo abstracto (general y **por arriba**) con lo concreto (particular y **por debajo**).\n",
+ "\n",
+ "En el marco del paradigma **POO** una subclase \"hereda\" los rasgos (estados y comportamientos) de su superclase (base), además puede contar con los suyos propios, en consecuencia, se trata de una *extensión* que *deriva* del objeto original y que dota el diseño de elasticidad tal que se permita evolucionar a nuevas versiones sin quebrar algún bloque de código ya existente mediante la colaboración entre objetos que permite la herencia.\n",
+ "La Herencia es pues, la habilidad para construir nuevas clases encima de las existentes, su beneficio más bondadoso es reusar código. Aunque el término mañosamente sugiere una \"jerarquía\" no se trata de una relación jerárquica solamente, sino epistemológica, esto es, la posibilidad de entender las unas en términos de las otras por aquello que las une, por ejemplo, Profesor-Estudiante (Asociación), Profesor-Salario (Dependencia), Universidad-Facultad (Composición) o Facultad-Profesor (Agregación) como se vio en el grafo anterior, por tanto *Herencia* se debe entender en éste contexto como se entendía en latín: «Estar adherido». Ver sección [Patrones de Diseño](#1-nociones-patrones) para intuir cómo favorecer una interacción sobre otra y concebir un diseño fértil y con alto grado de cohesión.\n",
+ "\n",
+ "*Nota*: Cualquier clase puede implementar varias interfaces a la vez, pero si alguna de ésas clases es una superclase entonces todas las respectivas subclases de dicha superclase deben implementar también las interfaces que su superclase implementa (incluso si no se usarán). Ver principio [DIP](#di-principle) para ahondar en buenas prácticas de dependencias.\n",
+ "\n",
+ "\n",
+ "#### Ejemplo 3 \n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\"\"\"Contrato de profesores.\n",
+ "\n",
+ "Ejemplo que ilustra el uso de herencia por medio de dos tipos\n",
+ "de universidades una pública y otra privada que\n",
+ "contratan ingenieros para ser profesores.\n",
+ "Note la sintaxis de un ``Professor`` que hereda de dos clases\n",
+ "a la vez, a ésto se le llama herencia múltiple.\n",
+ "Por el bien del ejemplo se asume, que por alguna razón misteriosa,\n",
+ "algún profesor pasa a estar inactivo pero a su vez en planilla.\n",
+ "En cuyo caso el algoritmo lo identifica y lo saca.\n",
+ "\n",
+ "Si algún método abstracto de la interfaz no es reescrito\n",
+ "por la clase concreta salta la excepción: ``NotImplementedError``\n",
+ "como podría ser el caso de una ``PrivateCollege``\n",
+ "que solitase al un ``Engineer`` para firmar un plano.\n",
+ "\"\"\"\n",
+ "\n",
+ "\n",
+ "class College():\n",
+ " \"\"\"Pseudo clase abstracta.\n",
+ "\n",
+ " Declara roles que la atañen a una Universidad.\n",
+ " \"\"\"\n",
+ "\n",
+ " # Pseudo métodos abstractos\n",
+ " def add_professor():\n",
+ " raise NotImplementedError(\"Función para contratar no actualizada.\")\n",
+ "\n",
+ " def rm_professor():\n",
+ " raise NotImplementedError(\"Función para despedir no actualizada.\")\n",
+ "\n",
+ " def email_professor():\n",
+ " raise NotImplementedError(\"Función para contactar no actualizada.\")\n",
+ "\n",
+ " def sign_design():\n",
+ " raise NotImplementedError(\"Colegiatura no disponible.\")\n",
+ "\n",
+ "\n",
+ "class Person():\n",
+ " def __init__(self, full_name: str, id_num: int) -> None:\n",
+ " self.full_name = full_name\n",
+ " self.id_number = id_num\n",
+ "\n",
+ "\n",
+ "class Engineer():\n",
+ " def __init__(self, resume: dict, union_membership: bool) -> None:\n",
+ " self.resume = resume\n",
+ " self.union_membership = union_membership\n",
+ "\n",
+ "\n",
+ "class Professor(Person, Engineer):\n",
+ " def __init__(self, full_name, id_num, resume, union_membership) -> None:\n",
+ " Person.__init__(self, full_name, id_num)\n",
+ " Engineer.__init__(self, resume, union_membership)\n",
+ "\n",
+ "\n",
+ "class PublicCollege(College):\n",
+ " \"\"\"Universidad Pública.\"\"\"\n",
+ "\n",
+ " def __init__(self) -> None:\n",
+ " self.register: list[Professor] = []\n",
+ " self.teachers_data: dict[str, object] = {}\n",
+ " self.salary: float = 150000\n",
+ "\n",
+ " def add_professor(self, professor: Professor) -> None:\n",
+ " \"\"\"Agregrar profesor si cumple requisitos.\"\"\"\n",
+ " if professor.resume and professor.union_membership:\n",
+ " # Agregar datos de profesor\n",
+ " name = professor.full_name\n",
+ " self.teachers_data[name] = professor.resume\n",
+ " self.teachers_data[name]['Salary'] = self.salary\n",
+ " # Agregar un profesor\n",
+ " professor.active = True\n",
+ " self.register.append(professor)\n",
+ " print(f\"Profesor <{name}> añadido.\")\n",
+ "\n",
+ " def rm_professor(self) -> None:\n",
+ " \"\"\"Eliminar profesores.\n",
+ "\n",
+ " Remover aquellos profesores que siendo de una\n",
+ " universidad pública no están activos pero por alguna\n",
+ " razón siguen en plantilla.\n",
+ " \"\"\"\n",
+ " # Recorrer registro de personal\n",
+ " rm_teachers = [teacher for teacher in self.register\n",
+ " if not teacher.active]\n",
+ "\n",
+ " # Eliminar en caso de que se encuentre activo\n",
+ " if rm_teachers:\n",
+ " for person in rm_teachers:\n",
+ " if person.full_name in self.teachers_data:\n",
+ " _ = self.teachers_data.pop(person.full_name)\n",
+ " print(f\"Profesor <{person.full_name}> sacado de planilla.\")\n",
+ " else:\n",
+ " print(\"Ningún profesor desactivado está en plantilla.\")\n",
+ "\n",
+ " def email_professor(self, email: str, content: str) -> str:\n",
+ " \"\"\"Enviar correo importante.\"\"\"\n",
+ " email_content = f\"Noticar {email} acerca de {content}.\"\n",
+ " return email_content\n",
+ "\n",
+ " def sign_design(self, professor: Professor) -> str:\n",
+ " \"\"\"Firmar plano.\"\"\"\n",
+ " sign_plane = f\"El plano ha sido firmado por ing. {professor.full_name}\"\n",
+ " return sign_plane\n",
+ "\n",
+ "\n",
+ "class PrivateCollege(College):\n",
+ " \"\"\"Universidad Privada.\n",
+ "\n",
+ " No demanda estar colegiado.\n",
+ " \"\"\"\n",
+ "\n",
+ " def __init__(self) -> None:\n",
+ " self.register: list[Professor] = []\n",
+ " self.teachers_data: dict[str, object] = {}\n",
+ " self.salary: float = 250000\n",
+ "\n",
+ " def add_professor(self, professor: Professor) -> None:\n",
+ " \"\"\"Agregrar profesor si cumple requisitos.\"\"\"\n",
+ " if professor.resume:\n",
+ " # Agregar datos de profesor\n",
+ " name = professor.full_name\n",
+ " self.teachers_data[name] = professor.resume\n",
+ " self.teachers_data[name]['Salary'] = self.salary\n",
+ " # Agregar un profesor\n",
+ " professor.active = True\n",
+ " self.register.append(professor)\n",
+ " print(f\"Profesor <{name}> añadido.\")\n",
+ "\n",
+ " def rm_professor(self) -> None:\n",
+ " \"\"\"Eliminar profesores.\n",
+ "\n",
+ " Remover aquellos profesores que siendo de una\n",
+ " universidad privada no están activos pero por alguna\n",
+ " razón siguen en plantilla.\n",
+ " \"\"\"\n",
+ " # Recorrer registro de personal\n",
+ " rm_teachers = [teacher for teacher in self.register\n",
+ " if not teacher.active]\n",
+ "\n",
+ " # Eliminar en caso de que se encuentre activo\n",
+ " if rm_teachers:\n",
+ " for person in rm_teachers:\n",
+ " if person.full_name in self.teachers_data:\n",
+ " _ = self.teachers_data.pop(person.full_name)\n",
+ " print(f\"Profesor <{person.full_name}> sacado de planilla.\")\n",
+ " else:\n",
+ " print(\"Ningún profesor desactivado está en plantilla.\")\n",
+ "\n",
+ " def email_professor(self, email: str, content: str) -> str:\n",
+ " \"\"\"Enviar correo importante.\"\"\"\n",
+ " email_content = f\"Noticar {email} acerca de {content}.\"\n",
+ " return email_content\n",
+ "\n",
+ "\n",
+ "def run_example() -> None:\n",
+ " nemo_resume = {\n",
+ " \"Proyectos\": 73,\n",
+ " \"Idiomas\": 3,\n",
+ " \"Publicaciones\": 13\n",
+ " }\n",
+ " troya_resume = {\n",
+ " \"Proyectos\": 11,\n",
+ " \"Idiomas\": 2,\n",
+ " \"Publicaciones\": 5\n",
+ " }\n",
+ "\n",
+ " nemo = Professor(\"Nemo Fuu\", 2357111317,\n",
+ " resume=nemo_resume, union_membership=True)\n",
+ " troya = Professor(\"Troya Horse\", 1713117532,\n",
+ " resume=troya_resume, union_membership=False)\n",
+ "\n",
+ " public_college = PublicCollege()\n",
+ " private_college = PrivateCollege()\n",
+ " public_college.add_professor(nemo)\n",
+ " private_college.add_professor(troya)\n",
+ "\n",
+ " private_college.register[-1].active = False\n",
+ " private_college.rm_professor()\n",
+ "\n",
+ "\n",
+ "if __name__ == \"__main__\":\n",
+ " run_example()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "En el [ejemplo 3](#ejemplo-3) se definió una **clase base** `College`, como superclase que delega comportamientos e impone una suerte de \"contrato\" imitando una interfaz, más formalmente es una imitación de una *clase abstracta*, la cual cuenta con métodos abstractos tales como `add_professor()` y `rm_professor()` y otros los cuales se inicializan para disparar un `NotImplementedError` cuando alguno es llamado sin ser debidamente reescrito. La clase concreta `PrivateCollege` (subclase) hereda de `College` pero no reescribe el método `sign_design()`. Por tanto, si se osa crear una instancia llamada `private_college` y seguidamente llamar dicho método, se produciría un error, indicando que el método necesita ser reescrito por la clase concreta en cuestión.\n",
+ "\n",
+ "Tener en cuenta que el abordaje que Python da a las interfaces es más flexible gracias al tipado *pato* éste es: «un objeto que parece pato, camina como pato y ¡quacks! como pato, ¡entonces és un pato!»; en vez de interfaces explícitas como en otros lenguajes. A diferencia de *Java* o *C#* en el arsenal de Python, no hay tal cosa como una \"Interfaz\" (no hace falta) pues su efecto se logra a través de **protocolos** o cargando el módulo nativo `abc` (abstract base classes) mezclado con excepciones personalizadas dado un contexto. El uso de `abc` encomienda ciertos patrones y responsabilidades algo así como la \"lista de deseos\" que clases subordinadas se ocupan -diligentes- de satisfacer. Su uso se verá más adelante.\n",
+ "\n",
+ "### 2.4. Polimorfismo \n",
+ "\n",
+ "Polimorfismo es la *cualidad* de tener muchas formas, pero en éste contexto ésa \"cualidad\" es de hecho una \"habilidad\" que se procura que los objetos tengan: «Todos deben *tratar* de saber hacer todo», ¡Eso sí! a su manera, porque de encontrarse en la incómoda situación de «perdirle peras al olmo» es porque algún principio de diseño se ha transgredido (ver principio [ISP](#is-principle)) con lo cual ése Titanic ya tiene su iceberg; en fin, se trata, de ganar diferentes implementaciones posibles para un mismo comportamiento de un *superobjeto*, como dijo Rimbaud «¿Y sin un trozo de madera descubre que es un violín?». En programación ésto se consigue casi siempre recargando y/o reescribiendo comportamientos ya sean métodos, funciones o incluso operaciones a punta de repartir responsabilidades de un contrato (rol o superclase) entre clases concretas (subclases), cosa que no es más que *extensión* con se vio en el ejemplo [ejemplo 3](#ejemplo-3), o implementación de interfaz además, en el caso de python, también por medio del uso de decoradores y métodos especiales i.e [métodos *dunder*](#special-meth) (**d**ouble **under**score). Con éstas astucias se le enseña al pedazo de madera que no sólo sirve de leña.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "*Respuestas*\n",
+ "Sr. Gauss: «El tiempo es la variable independiente por excelencia».\n",
+ "Sr. Borges: «Estar contigo o no estar contigo es la medida de mi tiempo».\n",
+ "Sr. Agustín: «Si nadie me pregunta qué es el tiempo, lo sé, si me lo preguntan, dejo de saberlo».\n",
+ "Sr. Alberto: «El tiempo es una abstracción que sirve para medir la sucesión».\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Definir el concepto de tiempo.\n",
+ "\n",
+ "Un programa cuyo personal docente es preguntado al azar\n",
+ "sobre el concepto del tiempo y cuya respuesta es según el bagaje y\n",
+ "campo del profesor correspondiente.\n",
+ "\"\"\"\n",
+ "\n",
+ "\n",
+ "class Professor():\n",
+ " \"\"\"Rol de profesor.\n",
+ "\n",
+ " Atributos\n",
+ " ---------\n",
+ " name : str\n",
+ " Nombre del profesor.\n",
+ "\n",
+ " Métodos\n",
+ " -------\n",
+ " define_time()\n",
+ " Definir tiempo según su rama.\n",
+ " \"\"\"\n",
+ "\n",
+ " def __init__(self, name: str) -> None:\n",
+ " \"\"\"Contrato con responsabilidades a delegar.\"\"\"\n",
+ " self.name = name\n",
+ "\n",
+ " # Método abstracto\n",
+ " def define_time(self):\n",
+ " \"\"\"Explicar el tiempo.\"\"\"\n",
+ " raise NotImplementedError(\"Método deber ser reescrito.\")\n",
+ "\n",
+ "\n",
+ "class Mathematician(Professor):\n",
+ " \"\"\"Matemático famoso.\"\"\"\n",
+ "\n",
+ " def __init__(self, name) -> None:\n",
+ " super().__init__(name=name)\n",
"\n",
+ " def define_time(self) -> str:\n",
+ " \"\"\"Definir matemáticamente el tiempo.\"\"\"\n",
+ " t_def = (\"El tiempo es la variable\",\n",
+ " \" independiente por excelencia\")\n",
+ " t_definition = f\"{t_def[0]}{t_def[1]}\"\n",
+ " return t_definition\n",
+ "\n",
+ "\n",
+ "class Poet(Professor):\n",
+ " \"\"\"Poeta de voz temblorosa.\"\"\"\n",
+ "\n",
+ " def __init__(self, name) -> None:\n",
+ " super().__init__(name=name)\n",
+ "\n",
+ " def define_time(self) -> str:\n",
+ " \"\"\"Definir poéticamente el tiempo.\"\"\"\n",
+ " t_def = (\"Estar contigo o no estar contigo es\",\n",
+ " \" la medida de mi tiempo\")\n",
+ " t_definition = f\"{t_def[0]}{t_def[1]}\"\n",
+ " return t_definition\n",
+ "\n",
+ "\n",
+ "class Philosopher(Professor):\n",
+ " \"\"\"Filósofo elocuente.\"\"\"\n",
+ "\n",
+ " def __init__(self, name) -> None:\n",
+ " super().__init__(name=name)\n",
+ "\n",
+ " def define_time(self) -> str:\n",
+ " \"\"\"Definir filosóficamente el tiempo.\"\"\"\n",
+ " t_def = (\"Si nadie me pregunta qué es el tiempo,\",\n",
+ " \" lo sé, si me lo preguntan, dejo de saberlo\")\n",
+ " t_definition = f\"{t_def[0]}{t_def[1]}\"\n",
+ " return t_definition\n",
+ "\n",
+ "\n",
+ "class Physicist(Professor):\n",
+ " \"\"\"Físico canoso.\"\"\"\n",
+ "\n",
+ " def __init__(self, name) -> None:\n",
+ " super().__init__(name)\n",
+ "\n",
+ " def define_time(self) -> str:\n",
+ " \"\"\"Definir tiempo físicamente.\"\"\"\n",
+ " t_def = (\"El tiempo es una abstracción que\",\n",
+ " \" sirve para medir la sucesión\")\n",
+ " t_definition = f\"{t_def[0]}{t_def[1]}\"\n",
+ " return t_definition\n",
+ "\n",
+ "\n",
+ "class Student():\n",
+ " \"\"\"Estudiante deseante de saber qué es el tiempo.\"\"\"\n",
+ "\n",
+ " def __init__(self, full_name: str) -> None:\n",
+ " self.name = full_name\n",
+ "\n",
+ " def ask_to_professor(self, professor: Professor) -> str:\n",
+ " \"\"\"Ask about time.\"\"\"\n",
+ " return professor.define_time()\n",
+ "\n",
+ "\n",
+ "def run_example() -> None:\n",
+ " template = {\"Gauss\": Mathematician,\n",
+ " \"Borges\": Poet,\n",
+ " \"Agustín\": Philosopher,\n",
+ " \"Alberto\": Physicist}\n",
+ " professors = [field(name) for name, field in template.items()]\n",
+ " student = Student(\"Nemo Fuu\")\n",
+ "\n",
+ " # Preguntar a algún profesor al azar\n",
+ " print(\"*Respuestas*\")\n",
+ " for i_professor in professors:\n",
+ " answer = student.ask_to_professor(i_professor)\n",
+ " # Mostrar definición según el profesor.\n",
+ " print(f\"Sr. {i_professor.name}: «{answer}».\")\n",
+ "\n",
+ "\n",
+ "if __name__ == \"__main__\":\n",
+ " run_example()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "En el ejemplo anterior, se derivaron una serie de clases concretas a saber: `Mathematician`, `Poet`, `Philosopher` y `Physicist` subordinadas que \"apuntan\" al contrato de una clase superior llamada `Professor`, es decir, dependen de una clase abstracta (informal) común que dicta las responsabilidades a ejecutar, hasta aquí todo es herencia o extensión, lo bello es lo siguiente, advierta que cuando se les exigó *indiscriminadamente* a cada tipo de profesor una definición del **tiempo** llamando el método `define_time()`, todos supieron responder y además lo hicieron según su naturaleza o sea, de manera dinámica sin siquiera conocer el contexto o el tipo de variable que és `i_professor` (ver principio [LSP](#ls-principle) para apalancar ésta estrategia).\n",
+ "Tener presente pues, que el método `ask_to_professor(proffesor)` depende de un `Professor`, no de un `Mathematician` o un `Poet` y demás. Por lo tanto, no importa quién responda al alumno. Podría ser Gauss, Borges, Agustín, o Alberto - no importa. Lo único que importa es que se suministre algún objeto con el rol de `Professor` y que implemente completamente los requisitos del contrato que le atañen a un profesor aunque cambie ropaje como el agua cuya forma se adapta a su envase y no por ello deja de ser agua. Así pues se resumen los pilares de POO: *Abstraer* es ignorar, *Encapsular* es ocultar, *Herencia* reusar, *Polimorfismo* malear y la interfaz el director de orquesta.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 3. Principios de diseño de software \n",
+ "\n",
+ "\n",
+ "### 3.1. Principios SOLID\n",
+ "El problema con los pilares de POO (Abstracción, Encapsulamiento, Herencia, Polimorfismo) es que se prestan a la subjetividad, no se sabe con claridad dónde termina uno y empieza el otro; no obstante, los principios de diseño S.O.L.I.D. son la cristalización de los pilares de POO y su apalancamiento en aras de un programa flexible, escalable, fácil de darle mantenimiento, con alto grado de cohesión (pero a la vez desacoplado) y trasegable, en suma, un programa Estético y más vale saber esgrimirlos sin perder de vista que «pragmatismo mata purismo».\n",
+ "\n",
+ "#### 3.1.1. Principio de responsabilidad única (*Single-Responsibility Principle*, **SRP**)\n",
+ "\n",
+ "> Una clase debe tener una sola razón para cambiar.\n",
+ "\n",
+ "Procurar que cada clase sea responsable de una única parte de la funcionalidad proporcionada por el software, y hacer que esa responsabilidad esté totalmente encapsulada (oculta) por la clase. O sea, una mordida a la vez, si una clase hace demasiadas cosas, ésta se tiene que modificar cada vez que cambia una de esas cosas. Al hacerlo, se corre el riesgo de quebrar otras partes dicha clase que ni siquiera se tenía intención de cambiar. El principal objetivo de este principio es reducir la complejidad por medio de desacople."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Precio total: 210\n",
+ "Procesando tipo de pago por débito\n",
+ "Verificando número de carné: 0372846\n",
+ "Nuevo estatus de matrícula: pagado\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Pago de matrícula.\n",
+ "\n",
+ "Antes de aplicar SRP.\n",
+ "\"\"\"\n",
+ "\n",
+ "\n",
+ "class Enrollment():\n",
+ " \"\"\"Estado de cuenta y carga académica.\"\"\"\n",
+ "\n",
+ " def __init__(self):\n",
+ " self.items = []\n",
+ " self.hours = []\n",
+ " self.prices = []\n",
+ " self.status = \"abierto\"\n",
+ "\n",
+ " def add_course(self, name, num_hrs, price):\n",
+ " self.items.append(name)\n",
+ " self.hours.append(num_hrs)\n",
+ " self.prices.append(price)\n",
+ "\n",
+ " def total_price(self):\n",
+ " total = 0\n",
+ " for i in range(len(self.prices)):\n",
+ " total += self.hours[i] * self.prices[i]\n",
+ " return total\n",
+ "\n",
+ " def pay(self, payment_type, carne_num):\n",
+ " if payment_type == \"débito\":\n",
+ " print(\"Procesando tipo de pago por débito\")\n",
+ " print(f\"Verificando número de carné: {carne_num}\")\n",
+ " self.status = \"pagado\"\n",
+ " elif payment_type == \"crédito\":\n",
+ " print(\"Procesando tipo de pago por crédito\")\n",
+ " print(f\"Verificando número de carné: {carne_num}\")\n",
+ " self.status = \"pagado\"\n",
+ " else:\n",
+ " raise Exception(f\"Tipo de pago desconocido: {payment_type}\")\n",
+ "\n",
+ "\n",
+ "enrol = Enrollment()\n",
+ "enrol.add_course(\"Física\", 1, 50)\n",
+ "enrol.add_course(\"Literatura\", 1, 150)\n",
+ "enrol.add_course(\"Redes\", 2, 5)\n",
+ "\n",
+ "print(\"Precio total: \", enrol.total_price())\n",
+ "enrol.pay(\"débito\", \"0372846\")\n",
+ "print(\"Nuevo estatus de matrícula: \", enrol.status)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Precio total: 210\n",
+ "Procesando tipo de pago por débito\n",
+ "Verificando número de carné: 0372846\n",
+ "Nuevo estatus de matrícula: pagado\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Pago de matrícula.\n",
+ "\n",
+ "Después de aplicar SRP.\n",
+ "\"\"\"\n",
+ "\n",
+ "\n",
+ "class PaymentProcessor():\n",
+ " \"\"\"Procesador de pago.\"\"\"\n",
+ "\n",
+ " def pay_debit(self, enrol, carne_num):\n",
+ " print(\"Procesando tipo de pago por débito\")\n",
+ " print(f\"Verificando número de carné: {carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ " def pay_credit(self, enrol, carne_num):\n",
+ " print(\"Procesando tipo de pago por crédito\")\n",
+ " print(f\"Verificando número de carné: {carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class Enrollment():\n",
+ " \"\"\"Estado de cuenta y carga académica.\"\"\"\n",
+ "\n",
+ " def __init__(self):\n",
+ " self.items = []\n",
+ " self.hours = []\n",
+ " self.prices = []\n",
+ " self.status = \"abierto\"\n",
+ "\n",
+ " def add_course(self, name, num_hrs, price):\n",
+ " self.items.append(name)\n",
+ " self.hours.append(num_hrs)\n",
+ " self.prices.append(price)\n",
+ "\n",
+ " def total_price(self):\n",
+ " total = 0\n",
+ " for i in range(len(self.prices)):\n",
+ " total += self.hours[i] * self.prices[i]\n",
+ " return total\n",
+ "\n",
+ "\n",
+ "enrol = Enrollment()\n",
+ "enrol.add_course(\"Física\", 1, 50)\n",
+ "enrol.add_course(\"Literatura\", 1, 150)\n",
+ "enrol.add_course(\"Redes\", 2, 5)\n",
+ "print(\"Precio total: \", enrol.total_price())\n",
+ "\n",
+ "processor = PaymentProcessor()\n",
+ "processor.pay_debit(enrol=enrol, carne_num=\"0372846\")\n",
+ "print(\"Nuevo estatus de matrícula: \", enrol.status)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "#### 3.1.2. Principio de abierto/cerrado (*Open-Closed Principle*, **OCP**)\n",
+ "\n",
+ "> Las clases deben estar abiertas a la extensión, pero cerradas a la modificación.\n",
+ "\n",
+ "La idea principal de este principio es evitar quebrar código existente al implementar nuevas funcionalidades. Se dice que una clase está *abierta* cuando está en el taller sometida al proceso de desarrollo, y *cerrada* cuando está acabada y lista para ser usada por otros *clientes* (clases mayores), la idea es que una vez que se cierra no debería haber ninguna restricción para incorporar nuevas características sin necesidad de mandar de nuevo al taller la infraestructura ya creada, en su lugar, debería poseer la habilidad de extenderse y, si así se requiriese, reusarse como base a ser reescrita. Tener presente que éste segundo aspecto no debe aplicarse sin juicio a todo cambio requerido, si la clase está mala se manda entera al taller de nuevo; no crear una subclase para ello: \"Una hija no es responsable de los problemas de la madre\".\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Precio total: 210\n",
+ "Procesando tipo de pago por PayPal\n",
+ "Verificando correo: student@ucr.ac.cr\n",
+ "Nuevo estatus de matrícula: pagado\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Aplicar OCP a ejemplo anterior.\n",
+ "\n",
+ "Note que ahora es posible incluso agregar otra forma\n",
+ "de pago ``CreditPaymentProcessor`` sin desbarajustar\n",
+ "estructura ya excistente. No obstante ésta nueva\n",
+ "clase solicita email en lugar de número de carné.\n",
+ "En el siguiente ejemplo se lidiará con eso.\n",
+ "\"\"\"\n",
+ "\n",
+ "from abc import ABC, abstractmethod\n",
+ "\n",
+ "\n",
+ "class PaymentProcessor(ABC):\n",
+ " \"\"\"Procesador de pago.\n",
+ "\n",
+ " Clase abstracta.\n",
+ " \"\"\"\n",
+ " @abstractmethod\n",
+ " def pay():\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "class CreditPaymentProcessor(PaymentProcessor):\n",
+ " def pay(self, enrol, carne_num):\n",
+ " print(\"Procesando tipo de pago por crédito\")\n",
+ " print(f\"Verificando número de carné: {carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class DebitPaymentProcessor(PaymentProcessor):\n",
+ " def pay(self, enrol, carne_num):\n",
+ " print(\"Procesando tipo de pago por débito\")\n",
+ " print(f\"Verificando número de carné: {carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class PaypalPaymentProcessor(PaymentProcessor):\n",
+ " def pay(self, enrol, email):\n",
+ " print(\"Procesando tipo de pago por PayPal\")\n",
+ " print(f\"Verificando correo: {email}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class Enrollment():\n",
+ " \"\"\"Estado de cuenta y carga académica.\"\"\"\n",
+ " def __init__(self):\n",
+ " self.items = []\n",
+ " self.hours = []\n",
+ " self.prices = []\n",
+ " self.status = \"abierto\"\n",
+ "\n",
+ " def add_course(self, name, num_hrs, price):\n",
+ " self.items.append(name)\n",
+ " self.hours.append(num_hrs)\n",
+ " self.prices.append(price)\n",
+ "\n",
+ " def total_price(self):\n",
+ " total = 0\n",
+ " for i in range(len(self.prices)):\n",
+ " total += self.hours[i] * self.prices[i]\n",
+ " return total\n",
+ "\n",
+ "\n",
+ "enrol = Enrollment()\n",
+ "enrol.add_course(\"Física\", 1, 50)\n",
+ "enrol.add_course(\"Literatura\", 1, 150)\n",
+ "enrol.add_course(\"Redes\", 2, 5)\n",
+ "print(\"Precio total: \", enrol.total_price())\n",
+ "\n",
+ "processor = PaypalPaymentProcessor()\n",
+ "processor.pay(enrol=enrol, email=\"student@ucr.ac.cr\")\n",
+ "print(\"Nuevo estatus de matrícula: \", enrol.status)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### 3.1.3. Principio de sustitución de Liskov (*Liskov Substitution Principle*, **LSP**) \n",
+ "\n",
+ "> Al extender una clase, la hija debe poder hacer el rol de madre.\n",
+ "\n",
+ "Esto significa que la subclase debe seguir siendo compatible y consistente con el comportamiento de la superclase. i.e. Al reescribir un método se debe extender el comportamiento base (original) es decir, estirarlo, en lugar de sustituirlo por algo completamente distinto, no sólo éso, para evitar sobreinterpretaciones, a la hora de estirar la clase base, vale la pena velar por algunos requerimientos, a saber:\n",
+ "\n",
+ "1. Los tipos de parámetros de un método de una subclase deben coincidir **o ser más abstractos** que los tipos de parámetros del método de la superclase.\n",
+ "1. El tipo de la variable retornada por un método de una subclase debe coincidir **o ser un subtipo** del tipo del tipo retorno del método de la superclase.\n",
+ "1. Un método de una subclase no debería disparar *tipos de* excepciones que su método base no se supone debería disparar de hecho esta regla ya está incorporada en lenguajes estáticos tales como *Java* o *C#*.\n",
+ "1. Una subclase no debería ser más conservadora con condiciones (restricciones) previas que su base establece.\n",
+ "1. Una subclase no debería ser más tolerante con tratamientos posteriores que su base ejecuta.\n",
+ "1. Las \"invariantes\" de una superclase se deben preservar. Entiéndase por *invariante* cómo aquellas cualidades y propiedades substanciales y que otra dependencia pueda asumir que el cliente tiene por ser cliente.\n",
+ "1. Una subclase no debería poder modificar aquellos valores (contenido) de campos (atributos) de su superclase que sean privados (aunque pueda tener acceso a ellos).\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Precio total: 210\n",
+ "Verificando código: 23571113\n",
+ "Procesando tipo de pago por PayPal\n",
+ "Verificando correo: student@ucr.ac.cr\n",
+ "Nuevo estatus de matrícula: pagado\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Aplicar LSP.\n",
+ "\n",
+ "Actulización de los campos (atributos) por medio\n",
+ "de constructor. Se agrega nueva funcionalidad para\n",
+ "autenticación por mensaje la cual se ataca en el\n",
+ "siguiente ejemplo de ISP.\n",
+ "\"\"\"\n",
+ "\n",
+ "from abc import ABC, abstractmethod\n",
+ "\n",
+ "\n",
+ "class PaymentProcessor(ABC):\n",
+ " \"\"\"Procesador de pago.\n",
+ "\n",
+ " Clase abstracta.\n",
+ " \"\"\"\n",
+ "\n",
+ " @abstractmethod\n",
+ " def pay():\n",
+ " pass\n",
+ "\n",
+ " @abstractmethod\n",
+ " def authen_sms():\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "class CreditPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, carne_num) -> None:\n",
+ " self.carne_num = carne_num\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " print(\"Procesando tipo de pago por crédito\")\n",
+ " print(f\"Verificando número de carné: {self.carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ " def authen_sms(self, code: int):\n",
+ " raise Exception(\"Crédito no soporta validación SMS.\")\n",
+ "\n",
+ "\n",
+ "class DebitPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, carne_num) -> None:\n",
+ " self.carne_num = carne_num\n",
+ " self.verified = False\n",
+ "\n",
+ " def authen_sms(self, code: int):\n",
+ " print(f\"Verificando código: {code}\")\n",
+ " self.verified = True\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " if not self.verified:\n",
+ " raise Exception(\"No autorizado.\")\n",
+ " print(\"Procesando tipo de pago por débito\")\n",
+ " print(f\"Verificando número de carné: {self.carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class PaypalPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, email) -> None:\n",
+ " self.email_address = email\n",
+ " self.verified = False\n",
+ "\n",
+ " def authen_sms(self, code: int):\n",
+ " print(f\"Verificando código: {code}\")\n",
+ " self.verified = True\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " if not self.verified:\n",
+ " raise Exception(\"No autorizado.\")\n",
+ " print(\"Procesando tipo de pago por PayPal\")\n",
+ " print(f\"Verificando correo: {self.email_address}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class Enrollment():\n",
+ " \"\"\"Estado de cuenta y carga académica.\"\"\"\n",
+ " def __init__(self):\n",
+ " self.items = []\n",
+ " self.hours = []\n",
+ " self.prices = []\n",
+ " self.status = \"abierto\"\n",
+ "\n",
+ " def add_course(self, name, num_hrs, price):\n",
+ " self.items.append(name)\n",
+ " self.hours.append(num_hrs)\n",
+ " self.prices.append(price)\n",
+ "\n",
+ " def total_price(self):\n",
+ " total = 0\n",
+ " for i in range(len(self.prices)):\n",
+ " total += self.hours[i] * self.prices[i]\n",
+ " return total\n",
+ "\n",
+ "\n",
+ "enrol = Enrollment()\n",
+ "enrol.add_course(\"Física\", 1, 50)\n",
+ "enrol.add_course(\"Literatura\", 1, 150)\n",
+ "enrol.add_course(\"Redes\", 2, 5)\n",
+ "print(\"Precio total: \", enrol.total_price())\n",
+ "\n",
+ "processor = PaypalPaymentProcessor(email=\"student@ucr.ac.cr\")\n",
+ "processor.authen_sms(code=23571113)\n",
+ "processor.pay(enrol=enrol)\n",
+ "print(\"Nuevo estatus de matrícula: \", enrol.status)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### 3.1.4. Principio de segregación de la interfaz (*Interface Segregation Principle*, **ISP**) \n",
+ "\n",
+ "> Clientes no deben estar obligados a depender de métodos que no utilizan pues interfaces se deben a los clientes y no a subniveles.\n",
+ "\n",
+ "No sólo no se le pide peras al olmo, sino que además no se debe obligar a hacerlo. Son los clientes los que definen, según su naturaleza, el rol de la clase abstracta. Según el principio de segregación de interfaces, en vez de una interfaz rellena de métodos hay que **añicarla** en otras más pequeñas que se ajusten a su respectivo cliente que la implementa. Los clientes sólo deben reescribir los métodos que realmente necesitan. De otro modo, un cambio en una interfaz de propósito general, es decir modificar alguno de sus contratos, da paso al quiebre de clientes que ni siquiera usaban dichos métodos recién modificados, además, aprovechando que no se limita el número de interfaces que una clase mayor (cliente) puede implementar simultáneamente, no hay necesidad de mezclar toneladas de responsabilidades, relacionadas o no, en una sola interfaz. \"Divir para vencer\"."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Precio total: 210\n",
+ "Verificando código SMS: 23571113\n",
+ "Procesando tipo de pago por PayPal\n",
+ "Verificando correo: Nemo.Fuu@ucr.ac.cr\n",
+ "Nuevo estatus de matrícula: pagado\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Aplicar ISP.\n",
+ "\n",
+ "El efecto de desmenuzar la interfaz\n",
+ "también se pudo haber conseguido por herencia, extendiendo\n",
+ "la clase abstracta ``PaymentProcessor``.\n",
+ "Sin embargo, en el siguiente abordaje, se crea la nueva\n",
+ "clase ``SMSAuthorizer`` para favorecer composición por\n",
+ "encima de herencia. Note que la interfaz ahora sólo posee\n",
+ "un rol ``.pay()`` y aquellas clases que no soportan\n",
+ "verificación por código SMS no tienen por qué reescribir\n",
+ "un método que no usan.\n",
+ "\"\"\"\n",
+ "\n",
+ "from abc import ABC, abstractmethod\n",
+ "\n",
+ "\n",
+ "class PaymentProcessor(ABC):\n",
+ " \"\"\"Procesador de pago sin validación.\n",
+ "\n",
+ " Clase abstracta.\n",
+ " \"\"\"\n",
+ "\n",
+ " @abstractmethod\n",
+ " def pay():\n",
+ " pass \n",
+ "\n",
+ "\n",
+ "class SMSAuthorizer():\n",
+ "\n",
+ " def __init__(self) -> None:\n",
+ " self.authorized = False\n",
+ "\n",
+ " def verify_code(self, code: int):\n",
+ " print(f\"Verificando código SMS: {code}\")\n",
+ " self.authorized = True\n",
+ "\n",
+ " def is_authorized(self) -> bool:\n",
+ " return self.authorized\n",
+ "\n",
+ "\n",
+ "class CreditPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, carne_num) -> None:\n",
+ " self.carne_num = carne_num\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " print(\"Procesando tipo de pago por crédito\")\n",
+ " print(f\"Verificando número de carné: {self.carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class DebitPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, carne_num, authorizer: SMSAuthorizer) -> None:\n",
+ " self.carne_num = carne_num\n",
+ " self.authorizer = authorizer\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " if not self.authorizer.is_authorized():\n",
+ " raise Exception(\"No autorizado.\")\n",
+ " print(\"Procesando tipo de pago por débito\")\n",
+ " print(f\"Verificando número de carné: {self.carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class PaypalPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, email, authorize: SMSAuthorizer) -> None:\n",
+ " self.email_address = email\n",
+ " self.authorize = authorize\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " if not self.authorize.is_authorized():\n",
+ " raise Exception(\"No autorizado.\")\n",
+ " print(\"Procesando tipo de pago por PayPal\")\n",
+ " print(f\"Verificando correo: {self.email_address}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class Enrollment():\n",
+ " \"\"\"Estado de cuenta y carga académica.\"\"\"\n",
+ " def __init__(self):\n",
+ " self.items = []\n",
+ " self.hours = []\n",
+ " self.prices = []\n",
+ " self.status = \"abierto\"\n",
+ "\n",
+ " def add_course(self, name, num_hrs, price):\n",
+ " self.items.append(name)\n",
+ " self.hours.append(num_hrs)\n",
+ " self.prices.append(price)\n",
+ "\n",
+ " def total_price(self):\n",
+ " total = 0\n",
+ " for i in range(len(self.prices)):\n",
+ " total += self.hours[i] * self.prices[i]\n",
+ " return total\n",
+ "\n",
+ "\n",
+ "enrol = Enrollment()\n",
+ "enrol.add_course(\"Física\", 1, 50)\n",
+ "enrol.add_course(\"Literatura\", 1, 150)\n",
+ "enrol.add_course(\"Redes\", 2, 5)\n",
+ "print(\"Precio total: \", enrol.total_price())\n",
+ "\n",
+ "authorizer = SMSAuthorizer()\n",
+ "authorizer.verify_code(23571113)\n",
+ "processor = PaypalPaymentProcessor(\"Nemo.Fuu@ucr.ac.cr\", authorizer)\n",
+ "\n",
+ "processor.pay(enrol=enrol)\n",
+ "print(\"Nuevo estatus de matrícula: \", enrol.status)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "#### 3.1.5. Principio de inversión de la dependencia (*Dependency Inversion Principle*, **DIP**) \n",
+ "\n",
+ "> Las clases **por encima** no debería depender de las clases **por debajo**. Ambas deben depender de su clase abstracta. Las abstracciones no dependen de lo concreto pero lo concreto debería depender de su abstracto.\n",
+ "\n",
+ "Clases *por debajo* se refiere a clases que implementan métodos \"gruesos\" para lidiar con los grandes rasgos del contrato, una \"pala\" por citar un ejemplo. Mientras que clases *por encima* se refiere a clases que les atañe lo fino y detallado, por ejemplo una \"cuchara\". La pala y la cuchara son parientes lejanos y de algún modo, de algún simbólico modo esperemos, una cumpliría la función de la otra. Es común, y no por ello infalible, que se diseñe primero para las de debajo, sin saber a veces a qué puerto se dirige, y luego para las que están por encima, es decir, agarrar la pala y pasarle lima; a través de ésta artimaña la lógica tiende a depender de clases por debajo hasta acabar errante. El principio postula **invertir** ésta dirección de dependencia, partir de lo que la clase por encima requiere y proveerle por medio de las de abajo (interfaz y abstractos) las operaciones pertinentes. Fijar la mira en la cuchara y por añadidura la \"retroexcavadora\" será concebida. A fin de cuentas, el fin justifica los medios."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Precio total: 210\n",
+ "Verificando código SMS: 23571113\n",
+ "Procesando tipo de pago por PayPal\n",
+ "Verificando correo: Nemo.Fuu@ucr.ac.cr\n",
+ "Nuevo estatus de matrícula: pagado\n"
+ ]
+ }
+ ],
+ "source": [
+ "\"\"\"Aplicar DIP.\n",
+ "\n",
+ "Las clases no deberían depender de una\n",
+ "clase concreta como puede ser: ``SMSAuthorizer``\n",
+ "sino de su abstracto, por lo cual se crea una\n",
+ "nueva interfaz hecha a la medida de un ``Authorizer``.\n",
+ "\"\"\"\n",
+ "\n",
+ "from abc import ABC, abstractmethod\n",
+ "\n",
+ "\n",
+ "class PaymentProcessor(ABC):\n",
+ " \"\"\"Procesador de pago sin validación.\n",
+ "\n",
+ " Clase abstracta.\n",
+ " \"\"\"\n",
+ "\n",
+ " @abstractmethod\n",
+ " def pay():\n",
+ " pass \n",
+ "\n",
+ "\n",
+ "class Authorizer(ABC):\n",
+ " \"\"\"Rol de autorizador.\"\"\"\n",
+ "\n",
+ " @abstractmethod\n",
+ " def is_authorized(self):\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "class SMSAuthorizer(Authorizer):\n",
+ "\n",
+ " def __init__(self) -> None:\n",
+ " self.authorized = False\n",
+ "\n",
+ " def verify_code(self, code: int):\n",
+ " print(f\"Verificando código SMS: {code}\")\n",
+ " self.authorized = True\n",
+ "\n",
+ " def is_authorized(self) -> bool:\n",
+ " return self.authorized\n",
+ "\n",
+ "\n",
+ "class CreditPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, carne_num) -> None:\n",
+ " self.carne_num = carne_num\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " print(\"Procesando tipo de pago por crédito\")\n",
+ " print(f\"Verificando número de carné: {self.carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class DebitPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, carne_num, authorizer: Authorizer) -> None:\n",
+ " self.carne_num = carne_num\n",
+ " self.authorizer = authorizer\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " if not self.authorizer.is_authorized():\n",
+ " raise Exception(\"No autorizado.\")\n",
+ " print(\"Procesando tipo de pago por débito\")\n",
+ " print(f\"Verificando número de carné: {self.carne_num}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class PaypalPaymentProcessor(PaymentProcessor):\n",
+ "\n",
+ " def __init__(self, email, authorize: Authorizer) -> None:\n",
+ " self.email_address = email\n",
+ " self.authorize = authorize\n",
+ "\n",
+ " def pay(self, enrol):\n",
+ " if not self.authorize.is_authorized():\n",
+ " raise Exception(\"No autorizado.\")\n",
+ " print(\"Procesando tipo de pago por PayPal\")\n",
+ " print(f\"Verificando correo: {self.email_address}\")\n",
+ " enrol.status = \"pagado\"\n",
+ "\n",
+ "\n",
+ "class Enrollment():\n",
+ " \"\"\"Estado de cuenta y carga académica.\"\"\"\n",
+ " def __init__(self):\n",
+ " self.items = []\n",
+ " self.hours = []\n",
+ " self.prices = []\n",
+ " self.status = \"abierto\"\n",
+ "\n",
+ " def add_course(self, name, num_hrs, price):\n",
+ " self.items.append(name)\n",
+ " self.hours.append(num_hrs)\n",
+ " self.prices.append(price)\n",
+ "\n",
+ " def total_price(self):\n",
+ " total = 0\n",
+ " for i in range(len(self.prices)):\n",
+ " total += self.hours[i] * self.prices[i]\n",
+ " return total\n",
+ "\n",
+ "\n",
+ "enrol = Enrollment()\n",
+ "enrol.add_course(\"Física\", 1, 50)\n",
+ "enrol.add_course(\"Literatura\", 1, 150)\n",
+ "enrol.add_course(\"Redes\", 2, 5)\n",
+ "print(\"Precio total: \", enrol.total_price())\n",
+ "\n",
+ "authorizer = SMSAuthorizer()\n",
+ "authorizer.verify_code(23571113)\n",
+ "processor = PaypalPaymentProcessor(\"Nemo.Fuu@ucr.ac.cr\", authorizer)\n",
+ "\n",
+ "processor.pay(enrol=enrol)\n",
+ "print(\"Nuevo estatus de matrícula: \", enrol.status)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 3.2. Nociones de los patrones de diseño \n",
+ "\n",
+ "Los patrones de diseño son soluciones típicas a problemas habituales en el diseño de software. Son como manuales prefabricados que puedes personalizar para resolver un problema de diseño recurrente en tu código.\n",
+ "\n",
+ "No basta con encontrar un patrón y copiarlo en el programa, como ocurre con las funciones o bibliotecas estándar. El patrón no es un fragmento de código específico, sino un concepto general para resolver un problema concreto. Puedes seguir los detalles del patrón e implementar una solución que se adapte a las realidades de tu propio programa.\n",
+ "\n",
+ "Los patrones se confunden a menudo con los algoritmos, porque ambos conceptos describen soluciones típicas a algunos problemas conocidos. Mientras que un algoritmo siempre define un conjunto claro de acciones que pueden lograr algún objetivo, un patrón es una descripción del nivel de detalle de una solución. El código del mismo patrón aplicado a dos programas distintos puede ser diferente.\n",
+ "\n",
+ "Una analogía de un algoritmo es una receta de cocina: ambos tienen pasos claros para alcanzar un objetivo. En cambio, un patrón es más parecido a un manual: puedes ver cuál es el resultado y sus características, pero el orden exacto de implementación depende de ti.\n",
+ "\n",
+ "La mayoría de los patrones se describen de manera muy formal para que la gente pueda reproducirlos en muchos contextos. Estas son las secciones que suelen estar presentes en la descripción de un patrón:\n",
+ "\n",
+ "- La **intención** del patrón describe brevemente tanto el problema como la solución.\n",
+ "\n",
+ "- La **motivación** explica con más detalle el problema y la solución que el patrón hace posible.\n",
+ "\n",
+ "- **Estructura** de clases muestra cada parte del patrón y cómo están relacionadas.\n",
+ "\n",
+ "- Un **ejemplo de código** en uno de los lenguajes de programación más populares facilita la comprensión de la idea que subyace al patrón.\n",
+ "\n",
+ "Algunos catálogos de patrones enumeran otros detalles útiles, como la aplicabilidad del patrón, los pasos de implementación y las relaciones con otros patrones.\n",
+ "\n",
+ "La verdad es que puedes llegar a trabajar como programador durante muchos años sin conocer ni un solo patrón. Mucha gente lo hace. Pero incluso en ese caso, puede que estés implementando algunos patrones sin ni siquiera saberlo. Entonces, ¿por qué dedicar tiempo a aprenderlos?\n",
+ "\n",
+ "- Los patrones de diseño son un conjunto de soluciones probadas a problemas comunes en el diseño de software. Incluso si nunca te encuentras con estos problemas, conocer los patrones sigue siendo útil porque te enseña a resolver todo tipo de problemas utilizando los principios del diseño orientado a objetos.\n",
+ "\n",
+ "- Los patrones de diseño definen un lenguaje común que usted y sus compañeros de equipo pueden utilizar para comunicarse de manera más eficiente. Puedes decir: \"Oh, utiliza un Singleton para eso\", y todo el mundo entenderá la idea que hay detrás de tu sugerencia. No hace falta explicar qué es un singleton si conoces el patrón y su nombre."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "### Más información\n",
"* Alexander Shvets (2021). \"Sumérgete en los patrones de diseño\". En [refactoring.guru](https://refactoring.guru/design-patterns/book).\n",
"* Leodanis Pozo Ramos (2023). \"SOLID Principles: Improve Object-Oriented Design in Python\". En [Real Python](https://realpython.com/solid-principles-python/)."
]
@@ -155,7 +1827,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.12"
+ "version": "3.10.11"
}
},
"nbformat": 4,