Una clase que hace tres trabajos
El principio de responsabilidad única dice que una clase debe tener un solo motivo para cambiar. Si una clase hace tres trabajos, un cambio en uno puede romper los otros dos.
Una tienda tiene una clase Factura que calcula el total, da formato al texto y lo guarda en un archivo. Cuando cambia el IVA, cambia el aspecto o cambia el sitio donde se guarda, hay que abrir la misma clase.
Al final de esta lección
Nota
Puedo detectar código que da dos trabajos a una clase o que cuesta ampliar.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Responsabilidad | un trabajo que una clase tiene a su cargo |
| Responsabilidad única | una clase, un solo motivo para cambiar |
| Cohesión | cuánto encaja lo que hace una clase |
| Abierto y cerrado | se amplía añadiendo código, no editando el que funciona |
| Extender | dar una función nueva sin tocar lo anterior |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una clase, un solo motivo para cambiar
Una responsabilidad es un trabajo, y cada trabajo cambia por una razón y a petición de una persona distinta.
Cuando una clase mezcla trabajos, tocar uno obliga a repasar los demás. Una clase con cohesión alta hace cosas que encajan entre sí.
Ejemplo: tres trabajos y tres motivos de cambio
| Trabajo | Quién pide el cambio | Un cambio posible |
|---|---|---|
| Calcular el total | Contabilidad | Cambia el IVA |
| Dar formato al texto | Diseño | Hay una cabecera nueva |
| Guardar la factura | Informática | Se guarda en otro sitio |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Cuenta los trabajos de una clase
Guarda la clase Factura con sus tres trabajos en factura.py. Con grep listas sus métodos y los cuentas. Cada método es una pista, no una prueba.
La orden grep -n "def " imprime cada línea que contiene def con su número de línea.
Entra en la carpeta de trabajo
mkdir -p ~/oop && cd ~/oopSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe una Factura que hace tres trabajos
printf 'class Factura:\n def __init__(self, cliente):\n self.cliente = cliente\n self.lineas = []\n\n def anadir(self, concepto, precio):\n self.lineas.append([concepto, precio])\n\n def total(self):\n suma = 0\n for linea in self.lineas:\n suma = suma + linea[1]\n return suma\n\n def como_texto(self):\n texto = "Cliente: " + self.cliente\n for linea in self.lineas:\n texto = texto + "\\n" + linea[0] + ": " + str(linea[1])\n return texto\n\n def guardar(self, archivo):\n with open(archivo, "w") as f:\n f.write(self.como_texto())\n' > factura.pySe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lista los métodos con su línea
grep -n "def " factura.pySe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Cuenta cuántos métodos hay
grep -c "def " factura.pySe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Usa la clase: total y texto
python3 -c "from factura import Factura; f = Factura('Ana'); f.anadir('cafe', 2); f.anadir('bocadillo', 4); print(f.total()); print(f.como_texto())"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un error frecuente
Advertencia
Mucha gente cree que una responsabilidad es un método. No es así. Una responsabilidad es un motivo para cambiar. Una clase puede tener cinco métodos y una sola responsabilidad. Si dos métodos cambian por razones distintas, sepáralos.
Se separa cada trabajo en su propia clase
La Factura guarda los datos y calcula el total. Otra clase da formato al texto. Otra la guarda. Cada una se puede probar sola.
Mira el código: FormateadorTexto recibe la factura y no la modifica. Si cambia la cabecera, solo se toca esa clase.
La factura y su formateador por separado
# Cada clase tiene un solo motivo para cambiar.
class Factura:
def __init__(self, cliente):
self.cliente = cliente
self.lineas = []
def anadir(self, concepto, precio):
self.lineas.append([concepto, precio])
def total(self):
suma = 0
for linea in self.lineas:
suma = suma + linea[1]
return suma
class FormateadorTexto:
def formatear(self, factura):
texto = "Cliente: " + factura.cliente
for linea in factura.lineas:
texto = texto + "\n" + linea[0] + ": " + str(linea[1])
return texto
factura = Factura("Ana")
factura.anadir("cafe", 2)
factura.anadir("bocadillo", 4)
print(factura.total())
print(FormateadorTexto().formatear(factura))
Abierto para extender, cerrado para modificar
Un cambio ideal se hace añadiendo código nuevo, sin editar el que ya funciona. Es el principio abierto y cerrado.
Una cadena de if y elif por tipo es una mala señal. Cada tipo nuevo obliga a editar la función. Con una clase por tipo, un formato nuevo es una clase nueva.
Cadena de if frente a clase por formato
# Mal: cada formato nuevo obliga a editar esta función.
def formatear(factura, tipo):
if tipo == "texto":
return texto_plano(factura)
elif tipo == "csv":
return texto_csv(factura)
# Un formato nuevo es otro elif aquí.
# Bien: cada formato es una clase con el mismo método.
class FormateadorCsv:
def formatear(self, factura):
return texto_csv(factura)
# Un formato nuevo es otra clase, y nada de lo anterior cambia.