Visi贸n general
Contenido
Este es el primer art铆culo de una breve serie dedicada a los patrones de dise帽o en Python.
Patrones de dise帽o creacional
Los Patrones de Dise帽o Creacional, como su nombre lo indica, se ocupan de la creaci贸n de clases u objetos.
Sirven para abstraer los detalles de las clases para que seamos menos dependientes de su implementaci贸n exacta, o para que no tengamos que lidiar con construcciones complejas cuando las necesitemos, o para garantizar algunas propiedades especiales de instanciaci贸n. .
Son muy 煤tiles para reducir el nivel de dependencia entre nuestras clases y controlar c贸mo el usuario interact煤a con ellas tambi茅n.
Los patrones de dise帽o cubiertos en este art铆culo son:
- F谩brica
- F谩brica abstracta
- Constructor
- Prototipo
- 煤nico
- Grupo de objetos
F谩brica
Problema
Supongamos que est谩 creando software para una compa帽铆a de seguros que ofrece seguros a personas que trabajan a tiempo completo. Hizo la aplicaci贸n usando una clase llamada Worker
.
Sin embargo, el cliente decide ampliar su negocio y ahora prestar谩 sus servicios tambi茅n a personas desempleadas, aunque con diferentes tr谩mites y condiciones.
隆Ahora tienes que hacer una clase completamente nueva para desempleados, que requerir谩 un constructor completamente diferente! Pero ahora no sabe a qu茅 constructor llamar en un caso general, mucho menos qu茅 argumentos pasarle.
Puede tener algunos condicionales desagradables en todo su c贸digo donde cada invocaci贸n de constructor est谩 rodeada de if
declaraciones, y usa alguna operaci贸n posiblemente costosa para verificar el tipo de objeto en s铆.
Si hay errores durante la inicializaci贸n, se detectan y el c贸digo se edita para hacerlo en cada uno de los cientos de lugares en los que se utilizan los constructores.
Sin estresarse para usted, es muy consciente de que este enfoque es menos que deseable, no escalable y totalmente insostenible.
Alternativamente, podr铆a considerar el patr贸n de f谩brica.
Soluci贸n
Las f谩bricas se utilizan para encapsular la informaci贸n sobre las clases que estamos usando, mientras las instanciamos en funci贸n de ciertos par谩metros que les proporcionamos.
Al usar una f谩brica, podemos cambiar una implementaci贸n por otra simplemente cambiando el par谩metro que se us贸 para decidir la implementaci贸n original en primer lugar.
Esto desacopla la implementaci贸n del uso de tal manera que podemos escalar f谩cilmente la aplicaci贸n agregando nuevas implementaciones y simplemente instanciandolas a trav茅s de la f谩brica, con exactamente la misma base de c贸digo.
Si solo obtenemos otra f谩brica como par谩metro, ni siquiera necesitamos saber qu茅 clase produce. Solo necesitamos tener un m茅todo de f谩brica uniforme que devuelva una clase garantizada para tener un cierto conjunto de comportamientos. Vamos a ver.
Para empezar, no olvide incluir m茅todos abstractos:
from abc import ABC, abstractmethod
Necesitamos que nuestras clases producidas implementen alg煤n conjunto de m茅todos que nos permitan trabajar con ellos de manera uniforme. Para ello implementamos la siguiente interfaz:
class Product(ABC):
@abstractmethod
def calculate_risk(self):
pass
Y ahora heredamos de 茅l a trav茅s de un Worker
y Unemployed
:
class Worker(Product):
def __init__(self, name, age, hours):
self.name = name
self.age = age
self.hours = hours
def calculate_risk(self):
# Please imagine a more plausible implementation
return self.age + 100/self.hours
def __str__(self):
return self.name+" ["+str(self.age)+"] - "+str(self.hours)+"h/week"
class Unemployed(Product):
def __init__(self, name, age, able):
self.name = name
self.age = age
self.able = able
def calculate_risk(self):
# Please imagine a more plausible implementation
if self.able:
return self.age+10
else:
return self.age+30
def __str__(self):
if self.able:
return self.name+" ["+str(self.age)+"] - able to work"
else:
return self.name+" ["+str(self.age)+"] - unable to work"
Ahora que tenemos a nuestra gente, hagamos su f谩brica:
class PersonFactory:
def get_person(self, type_of_person):
if type_of_person == "worker":
return Worker("Oliver", 22, 30)
if type_of_person == "unemployed":
return Unemployed("Sophie", 33, False)
Aqu铆, hemos codificado los par谩metros para mayor claridad, aunque normalmente solo crea una instancia de la clase y hace que haga lo suyo.
Para probar c贸mo funciona todo esto, creemos una instancia de nuestra f谩brica y dejemos que produzca un par de personas:
factory = PersonFactory()
product = factory.get_person("worker")
print(product)
product2 = factory.get_person("unemployed")
print(product2)
Oliver [22] - 30h/week
Sophie [33] - unable to work
F谩brica abstracta
Problema
Necesitas crear una familia de diferentes objetos. Aunque son diferentes, de alguna manera est谩n agrupados por un determinado rasgo.
Por ejemplo, es posible que deba crear un plato principal y un postre en un restaurante italiano y franc茅s, pero no mezclar谩 una cocina con la otra.
Soluci贸n
La idea es muy similar al patr贸n de f谩brica normal, la 煤nica diferencia es que todas las f谩bricas tienen m煤ltiples m茅todos separados para crear objetos, y el tipo de f谩brica es lo que determina la familia de objetos.
Una f谩brica abstracta es responsable de la creaci贸n de grupos enteros de objetos, junto con sus respectivas f谩bricas, pero no se preocupa por las implementaciones concretas de estos objetos. Esa parte queda para sus respectivas f谩bricas:
from abc import ABC, abstractmethod
class Product(ABC):
@abstractmethod
def cook(self):
pass
class FettuccineAlfredo(Product):
name = "Fettuccine Alfredo"
def cook(self):
print("Italian main course prepared: "+self.name)
class Tiramisu(Product):
name = "Tiramisu"
def cook(self):
print("Italian dessert prepared: "+self.name)
class DuckALOrange(Product):
name = "Duck 脌 L'Orange"
def cook(self):
print("French main course prepared: "+self.name)
class CremeBrulee(Product):
name = "Cr猫me br没l茅e"
def cook(self):
print("French dessert prepared: "+self.name)
class Factory(ABC):
@abstractmethod
def get_dish(type_of_meal):
pass
class ItalianDishesFactory(Factory):
def get_dish(type_of_meal):
if type_of_meal == "main":
return FettuccineAlfredo()
if type_of_meal == "dessert":
return Tiramisu()
def create_dessert(self):
return Tiramisu()
class FrenchDishesFactory(Factory):
def get_dish(type_of_meal):
if type_of_meal == "main":
return DuckALOrange()
if type_of_meal == "dessert":
return CremeBrulee()
class FactoryProducer:
def get_factory(self, type_of_factory):
if type_of_factory == "italian":
return ItalianDishesFactory
if type_of_factory == "french":
return FrenchDishesFactory
Podemos probar los resultados creando ambas f谩bricas y llamando a los cook()
m茅todos respectivos en todos los objetos:
fp = FactoryProducer()
fac = fp.get_factory("italian")
main = fac.get_dish("main")
main.cook()
dessert = fac.get_dish("dessert")
dessert.cook()
fac1 = fp.get_factory("french")
main = fac1.get_dish("main")
main.cook()
dessert = fac1.get_dish("dessert")
dessert.cook()
Italian main course prepared: Fettuccine Alfredo
Italian dessert prepared: Tiramisu
French main course prepared: Duck 脌 L'Orange
French dessert prepared: Cr猫me br没l茅e
Constructor
Problema
Necesitas representar un robot con la estructura de tu objeto. El robot puede ser humanoide con cuatro extremidades y estar de pie hacia arriba, o puede ser parecido a un animal con cola, alas, etc.
Puede usar ruedas para moverse, o puede usar palas de helic贸ptero. Puede usar c谩maras, un m贸dulo de detecci贸n de infrarrojos … te haces una idea.
Imagina el constructor de esta cosa:
def __init__(self, left_leg, right_leg, left_arm, right_arm,
left_wing, right_wing, tail, blades, cameras,
infrared_module, #...
):
self.left_leg = left_leg
if left_leg == None:
bipedal = False
self.right_leg = right_leg
self.left_arm = left_arm
self.right_arm = right_arm
# ...
Instanciar esta clase ser铆a extremadamente ilegible, ser铆a muy f谩cil equivocar algunos de los tipos de argumentos ya que estamos trabajando en Python y es dif铆cil de manejar acumular innumerables argumentos en un constructor.
Adem谩s, 驴qu茅 pasa si no queremos que el robot implemente todos los campos dentro de la clase? 驴Qu茅 pasa si queremos que solo tenga piernas en lugar de tener ambas piernas y ruedas?
Python no admite la sobrecarga de constructores, lo que nos ayudar铆a a definir tales casos (e incluso si pudi茅ramos, solo conducir铆a a constructores a煤n m谩s complicados).
Soluci贸n
Podemos crear una clase Builder que construya nuestro objeto y agregue los m贸dulos apropiados a nuestro robot. En lugar de un constructor complicado, podemos instanciar un objeto y agregar los componentes necesarios usando funciones.
Llamamos a la construcci贸n de cada m贸dulo por separado, despu茅s de instanciar el objeto. Sigamos adelante y definamos a Robot
con algunos valores predeterminados:
class Robot:
def __init__(self):
self.bipedal = False
self.quadripedal = False
self.wheeled = False
self.flying = False
self.traversal = []
self.detection_systems = []
def __str__(self):
string = ""
if self.bipedal:
string += "BIPEDAL "
if self.quadripedal:
string += "QUADRIPEDAL "
if self.flying:
string += "FLYING ROBOT "
if self.wheeled:
string += "ROBOT ON WHEELSn"
else:
string += "ROBOTn"
if self.traversal:
string += "Traversal modules installed:n"
for module in self.traversal:
string += "- " + str(module) + "n"
if self.detection_systems:
string += "Detection systems installed:n"
for system in self.detection_systems:
string += "- " + str(system) + "n"
return string
class BipedalLegs:
def __str__(self):
return "two legs"
class QuadripedalLegs:
def __str__(self):
return "four legs"
class Arms:
def __str__(self):
return "four legs"
class Wings:
def __str__(self):
return "wings"
class Blades:
def __str__(self):
return "blades"
class FourWheels:
def __str__(self):
return "four wheels"
class TwoWheels:
def __str__(self):
return "two wheels"
class CameraDetectionSystem:
def __str__(self):
return "cameras"
class InfraredDetectionSystem:
def __str__(self):
return "infrared"
Observe que hemos omitido inicializaciones espec铆ficas en el constructor y, en su lugar, hemos utilizado valores predeterminados. Esto se debe a que usaremos las clases Builder para inicializar estos valores.
Primero, implementamos un generador abstracto que define nuestra interfaz para construir:
from abc import ABC, abstractmethod
class RobotBuilder(ABC):
@abstractmethod
def reset(self):
pass
@abstractmethod
def build_traversal(self):
pass
@abstractmethod
def build_detection_system(self):
pass
Ahora podemos implementar varios tipos de constructores que obedezcan a esta interfaz, por ejemplo, para un Android y para un autom贸vil aut贸nomo:
class AndroidBuilder(RobotBuilder):
def __init__(self):
self.product = Robot()
def reset(self):
self.product = Robot()
def get_product(self):
return self.product
def build_traversal(self):
self.product.bipedal = True
self.product.traversal.append(BipedalLegs())
self.product.traversal.append(Arms())
def build_detection_system(self):
self.product.detection_systems.append(CameraDetectionSystem())
class AutonomousCarBuilder(RobotBuilder):
def __init__(self):
self.product = Robot()
def reset(self):
self.product = Robot()
def get_product(self):
return self.product
def build_traversal(self):
self.product.wheeled = True
self.product.traversal.append(FourWheels())
def build_detection_system(self):
self.product.detection_systems.append(InfraredDetectionSystem())
驴Observa c贸mo implementan los mismos m茅todos, pero hay una estructura de objetos intr铆nsecamente diferente debajo, y el usuario final no necesita lidiar con los detalles de esa estructura?
Por supuesto, podr铆amos hacer una Robot
que pueda tener patas y ruedas, y el usuario tendr铆a que agregar cada una por separado, pero tambi茅n podemos hacer constructores muy espec铆ficos que agreguen solo un m贸dulo apropiado para cada “parte”.
Probemos con an AndroidBuilder
para construir un Android:
builder = AndroidBuilder()
builder.build_traversal()
builder.build_detection_system()
print(builder.get_product())
Ejecutar este c贸digo producir谩:
BIPEDAL ROBOT
Traversal modules installed:
- two legs
- four legs
Detection systems installed:
- cameras
Y ahora, usemos an AutonomousCarBuilder
para construir un autom贸vil:
builder = AutonomousCarBuilder()
builder.build_traversal()
builder.build_detection_system()
print(builder.get_product())
Ejecutar este c贸digo producir谩:
ROBOT ON WHEELS
Traversal modules installed:
- four wheels
Detection systems installed:
- infrared
La inicializaci贸n es mucho m谩s limpia y legible en comparaci贸n con el desordenado constructor de antes y tenemos la flexibilidad de agregar los m贸dulos que queremos.
Si los campos de nuestro producto utilizan constructores relativamente est谩ndar, incluso podemos crear un llamado Director para administrar los constructores particulares:
class Director:
def make_android(self, builder):
builder.build_traversal()
builder.build_detection_system()
return builder.get_product()
def make_autonomous_car(self, builder):
builder.build_traversal()
builder.build_detection_system()
return builder.get_product()
director = Director()
builder = AndroidBuilder()
print(director.make_android(builder))
Ejecutar este fragmento de c贸digo producir谩:
BIPEDAL ROBOT
Traversal modules installed:
- two legs
- four legs
Detection systems installed:
- cameras
Dicho esto, el patr贸n Builder no tiene mucho sentido en clases peque帽as y simples, ya que la l贸gica adicional para construirlas solo agrega m谩s complejidad.
Sin embargo, cuando se trata de clases grandes y complicadas con numerosos campos, como las redes neuronales multicapa, el patr贸n Builder es un salvavidas.
Prototipo
Problema
Necesitamos clonar un objeto, pero es posible que no sepamos su tipo exacto, par谩metros, es posible que no todos se asignen a trav茅s del propio constructor o que dependan del estado del sistema en un punto particular durante el tiempo de ejecuci贸n.
Si intentamos hacerlo directamente, agregaremos muchas dependencias ramificadas en nuestro c贸digo, y es posible que ni siquiera funcione al final.
Soluci贸n
El patr贸n de dise帽o de prototipos aborda el problema de copiar objetos deleg谩ndolo en los propios objetos. Todos los objetos que se pueden copiar deben implementar un m茅todo llamado clone
y usarlo para devolver copias exactas de s铆 mismos.
Sigamos adelante y definamos una clone
funci贸n com煤n para todas las clases secundarias y luego la heredemos de la clase principal:
from abc import ABC, abstractmethod
class Prototype(ABC):
def clone(self):
pass
class MyObject(Prototype):
def __init__(self, arg1, arg2):
self.field1 = arg1
self.field2 = arg2
def __operation__(self):
self.performed_operation = True
def clone(self):
obj = MyObject(self.field1, field2)
obj.performed_operation = self.performed_operation
return obj
Alternativamente, puede usar la deepcopy
funci贸n en lugar de simplemente asignar campos como en el ejemplo anterior:
class MyObject(Prototype):
def __init__(self, arg1, arg2):
self.field1 = arg1
self.field2 = arg2
def __operation__(self):
self.performed_operation = True
def clone(self):
return deepcopy(self)
El patr贸n Prototype puede ser realmente 煤til en aplicaciones a gran escala que instancian muchos objetos. A veces, copiar un objeto ya existente es menos costoso que crear una instancia de uno nuevo.
煤nico
Problema
Un Singleton es un objeto con dos caracter铆sticas principales:
- Puede tener como m谩ximo una instancia
- Debe ser accesible globalmente en el programa.
Ambas propiedades son importantes, aunque en la pr谩ctica a menudo escuchar谩 a las personas llamar a algo Singleton incluso si solo tiene una de estas propiedades.
Tener solo una instancia suele ser un mecanismo para controlar el acceso a alg煤n recurso compartido. Por ejemplo, dos subprocesos pueden funcionar con el mismo archivo, por lo que en lugar de que ambos lo abran por separado, un Singleton puede proporcionar un punto de acceso 煤nico para ambos.
La accesibilidad global es importante porque despu茅s de que se haya creado una instancia de su clase una vez, deber谩 pasar esa instancia 煤nica para trabajar con ella. No se puede volver a crear una instancia. Por eso es m谩s f谩cil asegurarse de que cada vez que intente crear una instancia de la clase nuevamente, obtenga la misma instancia que ya tuvo.
Soluci贸n
Avancemos e implementemos el patr贸n Singleton haciendo que un objeto sea accesible globalmente y limitado a una sola instancia:
from typing import Optional
class MetaSingleton(type):
_instance : Optional[type] = None
def __call__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instance
class BaseClass:
field = 5
class Singleton(BaseClass, metaclass=MetaSingleton):
pass
Optional
aqu铆 hay un tipo de datos que puede contener una clase indicada en []
o None
.
La definici贸n de un __call__
m茅todo le permite usar instancias de la clase como funciones. El m茅todo tambi茅n se llama durante la inicializaci贸n, por lo que cuando llamamos a algo como a = Singleton()
debajo del cap贸, llamar谩 al __call__
m茅todo de su clase base .
En Python, todo es un objeto. Eso incluye clases. Todas las clases habituales que escribe, as铆 como las clases est谩ndar, tienen type
como tipo de objeto. Incluso type
es de tipo type
.
Lo que esto significa es que type
es una metaclase: otras clases son instancias de type
, al igual que los objetos variables son instancias de esas clases. En nuestro caso, Singleton
es una instancia de MetaSingleton
.
Todo esto significa que __call__
se llamar谩 a nuestro m茅todo cada vez que se cree un nuevo objeto y proporcionar谩 una nueva instancia si a煤n no hemos inicializado una. Si es as铆, solo devolver谩 la instancia ya inicializada.
super(MetaSingleton, cls).__call__(*args, **kwargs)
llama a la superclase ‘ __call__
. Nuestra superclase en este caso es type
, que tiene una __call__
implementaci贸n que realizar谩 la inicializaci贸n con los argumentos dados.
Hemos especificado nuestro tipo ( MetaSingleton
), el valor que se asignar谩 al _instance
campo ( cls
) y otros argumentos que podamos estar pasando.
En este caso, el prop贸sito de usar una metaclase en lugar de una implementaci贸n m谩s simple es esencialmente la capacidad de reutilizar el c贸digo.
En este caso, derivamos una clase de ella, pero si necesit谩ramos otro Singleton para otro prop贸sito, podr铆amos derivar la misma metaclase en lugar de implementar esencialmente lo mismo.
Ahora podemos intentar usarlo:
a = Singleton()
b = Singleton()
a == b
True
Debido a su punto de acceso global, es aconsejable integrar la seguridad de subprocesos en Singleton. Afortunadamente, no tenemos que editarlo demasiado para hacer eso. Simplemente podemos editar MetaSingleton
ligeramente:
def __call__(cls, *args, **kwargs):
with cls._lock:
if not cls._instance:
cls._instance = super().__call__(*args, **kwargs)
return cls._instance
De esta manera, si dos subprocesos comienzan a crear una instancia del Singleton al mismo tiempo, uno se detendr谩 en el bloqueo. Cuando el administrador de contexto libera el bloqueo, el otro ingresar谩 la if
declaraci贸n y ver谩 que la instancia ya ha sido creada por el otro hilo.
Grupo de objetos
Problema
Tenemos una clase en nuestro proyecto, llam茅moslo MyClass
. MyClass
es muy 煤til y se utiliza a menudo durante todo el proyecto, aunque durante per铆odos cortos de tiempo.
Sin embargo, su instanciaci贸n e inicializaci贸n son muy costosas y nuestro programa se ejecuta muy lentamente porque necesita constantemente crear nuevas instancias solo para usarlas en algunas operaciones.
Soluci贸n
Crearemos un grupo de objetos que se instanciar谩n cuando creemos el grupo en s铆. Siempre que necesitemos usar el objeto de tipo MyClass
, lo adquiriremos del grupo, lo usaremos y luego lo soltaremos nuevamente en el grupo para usarlo nuevamente.
Si el objeto tiene alg煤n tipo de estado de inicio predeterminado, la liberaci贸n siempre lo reiniciar谩. Si el grupo se deja vac铆o, inicializaremos un nuevo objeto para el usuario, pero cuando el usuario haya terminado con 茅l, lo devolver谩n al grupo para que se use nuevamente.
Sigamos adelante y primero definamos MyClass
:
class MyClass:
# Return the resource to default setting
def reset(self):
self.setting = 0
class ObjectPool:
def __init__(self, size):
self.objects = [MyClass() for _ in range(size)]
def acquire(self):
if self.objects:
return self.objects.pop()
else:
self.objects.append(MyClass())
return self.objects.pop()
def release(self, reusable):
reusable.reset()
self.objects.append(reusable)
Y para probarlo:
pool = ObjectPool(10)
reusable = pool.acquire()
pool.release(reusable)
Tenga en cuenta que esta es una implementaci贸n b谩sica y que, en la pr谩ctica, este patr贸n se puede usar junto con Singleton para proporcionar un 煤nico grupo accesible globalmente.
Tenga en cuenta que la utilidad de este patr贸n se disputa en los lenguajes que utilizan el recolector de basura.
La asignaci贸n de objetos que solo ocupan memoria (es decir, que no requieren recursos externos) tiende a ser relativamente econ贸mica en dichos lenguajes, mientras que muchas referencias “en vivo” a objetos pueden ralentizar la recolecci贸n de basura porque GC pasa por todas las referencias.
Conclusi贸n
Con esto, hemos cubierto los patrones de dise帽o de creaci贸n m谩s importantes en Python: los problemas que resuelven y c贸mo los resuelven.
Estar familiarizado con los patrones de dise帽o es un conjunto de habilidades extremadamente 煤til para todos los desarrolladores, ya que brindan soluciones a problemas comunes que se encuentran en la programaci贸n.
Conociendo tanto las motivaciones como las soluciones, tambi茅n puede evitar encontrar accidentalmente un anti-patr贸n mientras trata de resolver un problema.