Patrones de dise帽o creativos en Python

    Visi贸n general

    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 ifdeclaraciones, 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 Workery 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 Robotcon 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 Robotque 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 AndroidBuilderpara 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 AutonomousCarBuilderpara 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 cloney usarlo para devolver copias exactas de s铆 mismos.

    Sigamos adelante y definamos una clonefunci贸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 deepcopyfunci贸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
    

    Optionalaqu铆 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 typecomo tipo de objeto. Incluso typees de tipo type.

    Lo que esto significa es que typees una metaclase: otras clases son instancias de type, al igual que los objetos variables son instancias de esas clases. En nuestro caso, Singletones 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 _instancecampo ( 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 MetaSingletonligeramente:

    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 ifdeclaraci贸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. MyClasses 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.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *