Patrones de diseño estructural en Python

Este es el segundo artículo de una breve serie dedicada a los patrones de diseño en Python. Patrones de diseño estructural Los patrones de diseño estructural se utilizan para ensamblar...

Visión general

This is the second article in a short series dedicated to Patrones de diseño en Python.

Patrones de diseño estructural {#patrones de diseño estructural}

Los patrones de diseño estructural se utilizan para ensamblar múltiples clases en estructuras de trabajo más grandes.

A veces, las interfaces para trabajar con múltiples objetos simplemente no encajan, o está trabajando con código heredado que no puede cambiar pero necesita una nueva funcionalidad, o simplemente comienza a notar que sus estructuras parecen desordenadas y excesivas, pero todo elementos parecen necesarios.

Son muy útiles para crear código en capas legible y mantenible, especialmente cuando se trabaja con bibliotecas externas, código heredado, clases interdependientes o numerosos objetos.

Los patrones de diseño cubiertos en este artículo son:

Adaptador

En el mundo real, puede usar un adaptador para conectar cargadores a diferentes enchufes cuando viaja a otros países o diferentes modelos de teléfonos. Puede usarlos para conectar un monitor VGA antiguo a una toma HDMI en su nueva PC.

El patrón de diseño obtuvo su nombre porque su propósito es el mismo: adaptar una entrada a una salida predeterminada diferente.

Problema

Digamos que está trabajando en un software de visualización de imágenes y, hasta ahora, sus clientes solo querían mostrar [Imágenes de trama] (https://en.wikipedia.org/wiki/Raster_graphics). Tiene una implementación completa para dibujar, digamos, un archivo .png en la pantalla.

En aras de la simplicidad, así es como se ve la funcionalidad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from abc import ABC, abstractmethod

class PngInterface(ABC):
    @abstractmethod
    def draw(self):
        pass

class PngImage(PngInterface):
    def __init__(self, png):
        self.png = png
        self.format = "raster"
        
    def draw(self):
        print("drawing " + self.get_image())
            
    def get_image(self):
        return "png"

Pero desea ampliar su público objetivo ofreciendo más funciones, por lo que decide hacer que su programa funcione también para gráficos vectoriales.

Resulta que hay una biblioteca para trabajar con gráficos vectoriales que puede usar en lugar de implementar toda esa funcionalidad completamente nueva usted mismo. Sin embargo, las clases no se ajustan a su interfaz (no implementan el método draw()):

1
2
3
4
5
6
7
class SvgImage:
    def __init__(self, svg):
        self.svg = svg
        self.format = "vector"
        
    def get_image(self):
        return "svg"

No desea verificar el tipo de cada objeto antes de hacer algo con él, realmente le gustaría usar una interfaz uniforme, la que ya tiene.

Solución

Para resolver este problema, implementamos una clase Adaptador. Al igual que los adaptadores del mundo real, nuestra clase tomará el recurso disponible externamente (clase SvgImage) y lo convertirá en una salida que se adapte a nosotros.

En este caso, hacemos esto rasterizando la imagen vectorial para que podamos dibujarla usando la misma funcionalidad que ya hemos implementado.

Nuevamente, por simplicidad, simplemente imprimiremos "png", sin embargo, esa función dibujaría la imagen en la vida real.

Adaptador de objeto

Un Adaptador de objetos simplemente envuelve la clase externa (servicio), ofreciendo una interfaz que se ajusta a nuestra propia clase (cliente). En este caso, el servicio nos proporciona un gráfico vectorial, y nuestro adaptador realiza la rasterización y dibuja la imagen resultante:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class SvgAdapter(png_interface):
    def __init__(self, svg):
        self.svg = svg
        
    def rasterize(self):
        return "rasterized " + self.svg.get_image()
    
    def draw(self):
        img = self.rasterize()
        print("drawing " + img)

Así que probemos cómo funciona nuestro adaptador:

1
2
3
4
5
6
regular_png = PngImage("some data")
regular_png.draw()

example_svg = SvgImage("some data")
example_adapter = SvgAdapter(example_svg)
example_adapter.draw()

Pasar regular_png funciona bien para nuestra función graphic_draw(). Sin embargo, pasar un regular_svg no funciona. Al adaptar el objeto regular_svg, podemos usar su forma adaptada tal como usaríamos una imagen .png:

1
2
drawing png
drawing rasterized svg

No hay necesidad de cambiar nada dentro de nuestra función graphic_draw(). Funciona igual que antes. Simplemente adaptamos la entrada para adaptarla a la función ya existente.

Adaptador de clase

Adaptadores de clase solo se pueden implementar en lenguajes que soporten herencia múltiple. Heredan tanto nuestra clase como la clase externa, por lo que heredan todas sus funcionalidades. Debido a esto, una instancia del adaptador puede reemplazar nuestra clase o la clase externa, bajo una interfaz uniforme.

Para permitirnos hacer esto, necesitamos tener alguna forma de verificar si necesitamos realizar una transformación o no. Para comprobar esto, introducimos una excepción:

1
2
3
4
class ConvertingNonVector(Exception):
    # An exception used by class_adapter to check
    # whether an image can be rasterized
    pass

Y con eso, podemos hacer un adaptador de clase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class ClassAdapter(png_image, svg_image):
    def __init__(self, image):
        self.image = image
        
    def rasterize(self):
        if(self.image.format == "vector"):
            return "rasterized " + self.image.get_image()
        else:
            raise ConvertingNonVector
        
    def draw(self):
        try:
            img = self.rasterize()
            print("drawing " + img)
        except ConvertingNonVector as e:
            print("drawing " + self.image.get_image())

Para probar si funciona bien, probémoslo en ambas imágenes .png y .svg:

1
2
3
4
5
6
7
example_png = PngImage("some data")
regular_png = ClassAdapter(example_png)
regular_png.draw()

example_svg = SvgImage("some data")
example_adapter = ClassAdapter(example_svg)
example_adapter.draw()

Ejecutar este código da como resultado:

1
2
drawing png
drawing rasterized svg

¿Objeto o adaptador de clase? {#adaptador de clase de objeto}

En general, debería preferir usar Adaptadores de objetos. Hay dos razones principales para favorecerlo sobre su versión de clase, y esas son:

  • El Principio de composición sobre herencia que asegura un bajo acoplamiento. En el ejemplo anterior, el campo formato asumido no tiene que existir para que funcione el adaptador de objeto, mientras que es necesario para el adaptador de clase.
  • Complejidad añadida que puede dar lugar a problemas relacionados con la herencia múltiple.

Puente

Problema

Una clase grande puede violar El principio de responsabilidad única y es posible que deba dividirse en clases separadas, con jerarquías separadas. Esto puede extenderse aún más a una gran jerarquía de clases que debe dividirse en dos jerarquías separadas, pero interdependientes.

Por ejemplo, imagina que tenemos una estructura de clases que incluye edificios medievales. Tenemos un ‘muro’, ’torre’, ’establo’, ‘molino’, ‘casa’, ‘armería’, etc. Ahora queríamos diferenciarlos según los materiales de los que están hechos. Podríamos derivar cada clase y hacer pared_de_paja, pared_de_registro, pared_de_cobblestone, tower_de_piedra_caliza, etc...

Además, una ’torre’ podría extenderse a una ’torre de vigilancia’, ‘faro’ y ’torre_del_castillo’.

bad class structure

Pero esto daría como resultado un crecimiento exponencial del número de clases si continuáramos agregando atributos de manera similar. Además, estas clases tendrían mucho código repetido.

Además, ¿limestone_watchtower extendería limestone_tower y agregaría detalles de una torre de vigilancia o extendería watchtower y agregaría detalles de materiales?

Solución

Para evitar esto, sacaremos la información fundamental y la convertiremos en un terreno común sobre el cual construiremos variaciones. En nuestro caso, separaremos una jerarquía de clases para un ‘Edificio’ y ‘Material’.

Querremos tener un puente entre todas las subclases Building y todas las subclases Material para que podamos generar variaciones de ellas, sin tener que definirlas como clases separadas. Dado que un material se puede usar en muchas cosas, la clase “Edificio” contendrá “Material” como uno de sus campos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from abc import ABC, abstractmethod

class Material(ABC):
    @abstractmethod
    def __str__(self):
        pass
        
class Cobblestone(Material):
    def __init__(self):
        pass
    
    def __str__(self):
        return 'cobblestone'
        
class Wood(Material):
    def __init__(self):
        pass
    
    def __str__(self):
        return 'wood'

Y con eso, hagamos una clase Building:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from abc import ABC, abstractmethod       
        
class Building(ABC):
    @abstractmethod
    def print_name(self):
        pass
        
class Tower(Building):
    def __init__(self, name, material):
        self.name = name
        self.material = material
        
    def print_name(self):
        print(str(self.material) + ' tower ' + self.name)
        
class Mill(Building):
    def __init__(self, name, material):
        self.name = name
        self.material = material
        
    def print_name(self):
        print(str(self.material) + ' mill ' + self.name)

Ahora, cuando nos gustaría crear un molino de adoquines o una torre de madera, no necesitamos las clases CobblestoneMill o WoodenTower. En su lugar, podemos instanciar un ‘Molino’ o ‘Torre’ y asignarle cualquier material que deseemos:

1
2
3
4
5
6
7
cobb = Cobblestone()
local_mill = Mill('Hilltop Mill', cobb)
local_mill.print_name()

wooden = Wood()
watchtower = Tower('Abandoned Sentry', wooden)
watchtower.print_name()

Ejecutar este código produciría:

1
2
cobblestone mill Hilltop Mill
wooden tower Abandoned Sentry

Compuesto

Problema

Imagine que está ejecutando un servicio de entrega y los proveedores envían grandes cajas llenas de artículos a través de su empresa. Querrá saber el valor de los artículos que contiene porque cobra tarifas por paquetes de alto valor. Por supuesto, esto se hace automáticamente, porque tener que desenvolver todo es una molestia.

Esto no es tan simple como ejecutar un bucle, porque la estructura de cada cuadro es irregular. Puede recorrer los elementos del interior, claro, pero ¿qué sucede si una caja contiene otra caja con elementos en el interior? ¿Cómo puede tu bucle lidiar con eso?

Claro, puede verificar la clase de cada elemento en bucle, pero eso solo introduce más complejidad. Cuantas más clases tenga, más casos extremos habrá, lo que conducirá a un sistema no escalable.

Solución

Lo que es notable en problemas como estos es que tienen una estructura jerárquica similar a un árbol. Tienes la caja más grande, en la parte superior. Y luego tienes artículos más pequeños o en cajas en el interior. Una buena manera de lidiar con una estructura como esta es hacer que el objeto directamente encima controle el comportamiento de los que están debajo.

El patrón de diseño Compuesto se utiliza para componer estructuras en forma de árbol y tratar colecciones de objetos de manera similar.

En nuestro ejemplo, podríamos hacer que cada cuadro contenga una lista de su contenido y asegurarnos de que todos los cuadros y elementos tengan una función: return_price(). Si llamas a return_price() en una caja, recorre su contenido y suma sus precios (también se calcula llamando a su return_price()), y si tienes un artículo, simplemente devuelve su precio.

Hemos creado una situación tipo recursión donde resolvemos un gran problema dividiéndolo en problemas más pequeños e invocando la misma operación en ellos. Estamos, en cierto sentido, haciendo una búsqueda primero en profundidad a través de la jerarquía de objetos.

Definiremos una clase elemento abstracta, de la que heredarán todos nuestros elementos específicos:

1
2
3
4
5
6
from abc import ABC, abstractmethod

class Item(ABC):
    @abstractmethod
    def return_price(self):
        pass

Ahora, definamos algunos productos que nuestros proveedores pueden enviar a través de nuestra empresa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Box(Item):
    def __init__(self, contents):
        self.contents = contents
        
    def return_price(self):
        price = 0
        for item in self.contents:
            price = price + item.return_price()
        return price

class Phone(Item):
    def __init__(self, price):
        self.price = price
        
    def return_price(self):
        return self.price

class Charger(Item):
    def __init__(self, price):
        self.price = price
        
    def return_price(self):
        return self.price

class Earphones(Item):
    def __init__(self, price):
        self.price = price
        
    def return_price(self):
        return self.price

El Box en sí mismo también es un Item y podemos agregar una instancia de Box dentro de una instancia de Box. Vamos a instanciar algunos elementos y ponerlos en una caja antes de obtener su valor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
phone_case_contents = []
phone_case_contents.append(Phone(200))
phone_case_box = Box(phone_case_contents)

big_box_contents = []
big_box_contents.append(phone_case_box)
big_box_contents.append(Charger(10))
big_box_contents.append(Earphones(10))
big_box = Box(big_box_contents)

print("Total price: " + str(big_box.return_price()))

Ejecutar este código daría como resultado:

1
Total price: 220

Decorador

Problema

Imagina que estás haciendo un videojuego. La mecánica central de tu juego es que el jugador puede agregar diferentes potenciadores en medio de la batalla de un grupo aleatorio.

Esos poderes realmente no pueden simplificarse y colocarse en una lista que puede recorrer, algunos de ellos sobrescriben fundamentalmente cómo se mueve o apunta el personaje del jugador, algunos simplemente agregan efectos a sus poderes, algunos agregan funcionalidades completamente nuevas si presiona algo, etc.

Inicialmente, podría pensar en usar la herencia para resolver esto. Después de todo, si tienes basic_player, puedes heredar blazing_player, bouncy_player y bowman_player de él.

Pero, ¿qué pasa con blazing_bouncy_player, bouncy_bowman_player, blazing_bowman_player y blazing_bouncy_bowman_player?

A medida que agregamos más poderes, la estructura se vuelve cada vez más compleja, tenemos que usar herencia múltiple o repetir el código, y cada vez que agregamos algo al juego, es mucho trabajo hacer que funcione con todo lo demás.

Solución

El Patrón Decorador se usa para agregar funcionalidad a una clase sin cambiar la clase misma. La idea es crear un envoltorio que se ajuste a la misma interfaz que la clase que estamos envolviendo, pero anula sus métodos.

Puede llamar al método desde el objeto miembro y luego agregar algo de su propia funcionalidad sobre él, o puede anularlo por completo. El decorador (envoltorio) se puede envolver con otro decorador, que funciona exactamente igual.

De esta forma, podemos decorar un objeto tantas veces como queramos, sin cambiar ni un ápice la clase original. Avancemos y definamos un PlayerDecorator:

1
2
3
4
5
6
from abc import ABC, abstractmethod

class PlayerDecorator(ABC):
    @abstractmethod
    def handle_input(self, c):
        pass

Y ahora, definamos una clase BasePlayer, con algún comportamiento predeterminado y sus subclases, especificando un comportamiento diferente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class BasePlayer:
    def __init__(self):
        pass
    
    def handle_input(self, c):
        if   c=='w':
            print('moving forward')
        elif c == 'a':
            print('moving left')
        elif c == 's':
            print('moving back')
        elif c == 'd':
            print('moving right')
        elif c == 'e':
            print('attacking ')
        elif c == ' ':
            print('jumping')
        else:
            print('undefined command')
            
class BlazingPlayer(PlayerDecorator):
    def __init__(self, wrapee):
        self.wrapee = wrapee
        
    def handle_input(self, c):
        if c == 'e':
            print('using fire ', end='')
        
        self.wrapee.handle_input(c)
        
class BowmanPlayer(PlayerDecorator):
    def __init__(self, wrapee):
        self.wrapee = wrapee
        
    def handle_input(self, c):
        if c == 'e':
            print('with arrows ', end='')
            
        self.wrapee.handle_input(c)
        
class BouncyPlayer(PlayerDecorator):
    def __init__(self, wrapee):
        self.wrapee = wrapee
        
    def handle_input(self, c):
        if c == ' ':
            print('double jump')
        else:
            self.wrapee.handle_input(c)

Vamos a envolverlos uno por uno ahora, comenzando con un BasePlayer:

1
2
3
player = BasePlayer()
player.handle_input('e')
player.handle_input(' ')

Ejecutar este código devolvería:

1
2
attacking 
jumping

Ahora, envolvámoslo con otra clase que maneje estos comandos de manera diferente:

1
2
3
player = BlazingPlayer(player)
player.handle_input('e')
player.handle_input(' ')

Esto devolvería:

1
2
using fire attacking 
jumping

Ahora, agreguemos las características de BouncyPlayer:

1
2
3
player = BouncyPlayer(player)
player.handle_input('e')
player.handle_input(' ')
1
2
using fire attacking 
double jump

Lo que vale la pena señalar es que el jugador está usando un ataque de fuego, así como un salto doble. Estamos decorando el jugador con diferentes clases. Vamos a decorarlo un poco más:

1
2
3
player = BowmanPlayer(player)
player.handle_input('e')
player.handle_input(' ')

Esto devuelve:

1
2
with arrows using fire attacking 
double jump

Fachada

Problema

Digamos que estás haciendo una simulación de un fenómeno, tal vez un concepto evolutivo como el equilibrio entre diferentes estrategias. Estás a cargo del back-end y tienes que programar qué hacen los especímenes cuando interactúan, cuáles son sus propiedades, cómo funcionan sus estrategias, cómo llegan a interactuar entre sí, qué condiciones hacen que mueran o se reproduzcan. , etc.

Su colega está trabajando en la representación gráfica de todo esto. No les importa la lógica subyacente de su programa, varias funciones que verifican con quién está tratando el espécimen, guardan información sobre interacciones anteriores, etc.

Su estructura subyacente compleja no es muy importante para su colega, solo quiere saber dónde está cada espécimen y cómo se supone que debe verse.

Entonces, ¿cómo hace que su sistema complejo sea accesible para alguien que sepa poco de teoría de juegos y menos sobre su implementación particular de algún problema?

Solución

El Patrón de fachada exige una fachada de su implementación. Las personas no necesitan saber todo acerca de la implementación subyacente. Puede crear una gran clase que administre completamente su complejo subsistema y solo proporcione las funcionalidades que su usuario probablemente necesite.

En el caso de su colega, probablemente querrá poder pasar a la siguiente iteración de la simulación y obtener información sobre las coordenadas de los objetos y los gráficos apropiados para representarlos.

Digamos que el siguiente fragmento de código es nuestro "sistema complejo". Naturalmente, puedes omitir su lectura, ya que el punto es que no tienes que conocer los detalles para usarlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Hawk:
    def __init__(self):
        self.asset = '(`A´)'
        self.alive = True
        self.reproducing = False
    
    def move(self):
        return 'deflect'
    
    def reproduce(self):
        return hawk()
    
    def __str__(self):
        return self.asset
    
class Dove:
    def __init__(self):
        self.asset = '(๑•́ω•̀)'
        self.alive = True
        self.reproducing = False
    
    def move(self):
        return 'cooperate'
    
    def reproduce(self):
        return dove()
    
    def __str__(self):
        return self.asset
        
 def iteration(specimen):
    half = len(specimen)//2
    spec1 = specimen[:half]
    spec2 = specimen[half:]
    
    for s1, s2 in zip(spec1, spec2):
        move1 = s1.move()
        move2 = s2.move()
        
        if move1 == 'cooperate':
            # both survive, neither reproduce
            if move2 == 'cooperate':
                pass
            # s1 dies, s2 reproduces
            elif move2 == 'deflect':
                s1.alive = False
                s2.reproducing = True
        elif move1 == 'deflect':
            # s2 dies, s1 reproduces
            if move2 == 'cooperate':
                s2.alive = False
                s1.reproducing = True
            # both die
            elif move2 == 'deflect':
                s1.alive = False
                s2.alive = False
                
    s = spec1 + spec2
    s = [x for x in s if x.alive == True]
    
    for spec in s:
        if spec.reproducing == True:
            s.append(spec.reproduce())
            spec.reproducing = False
                
    return s

Ahora, darle este código a nuestro colega requerirá que se familiarice con el funcionamiento interno antes de intentar visualizar a los animales. En su lugar, pintemos una fachada sobre ella y démosles un par de funciones convenientes para iterar la población y acceder a animales individuales desde ella:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import random

class Simulation:
    def __init__(self, hawk_number, dove_number):
        self.population = []
        for _ in range(hawk_number):
            self.population.append(hawk())
        for _ in range(dove_number):
            self.population.append(dove())
        random.shuffle(self.population)
            
    def iterate(self):
        self.population = iteration(self.population)
        random.shuffle(self.population)
        
    def get_assets(self):
        return [str(x) for x in population]

Un lector curioso puede jugar llamando a iterate() y viendo qué sucede con la población.

peso mosca

Problema

Estás trabajando en un videojuego. Hay muchas viñetas en tu juego, y cada viñeta es un objeto separado. Tus balas tienen información única, como sus coordenadas y velocidad, pero también tienen información compartida, como forma y textura.

1
2
3
4
5
6
7
class Bullet:
    def __init__(self, x, y, z, velocity):
        self.x = x
        self.y = y
        self.z = z
        self.velocity = velocity
        self.asset = '■■►'

Esos consumirían una cantidad considerable de memoria, especialmente si hay muchas balas en el aire al mismo tiempo (y no guardaremos un emoticón Unicode en lugar de activos en la vida real).

Definitivamente sería preferible obtener la textura de la memoria una vez, tenerla en el caché y hacer que todas las viñetas compartan esa única textura, en lugar de copiarla docenas o cientos de veces.

Si se disparara un tipo diferente de bala, con una textura diferente, crearíamos una instancia de ambos y los devolveríamos. Sin embargo, si estamos tratando con valores duplicados, podemos mantener el valor original en un grupo/caché y simplemente extraer de allí.

Solución

El Patrón Flyweight requiere un grupo común cuando pueden existir muchas instancias de un objeto con el mismo valor. Una implementación famosa de esto es Java String Pool, donde si intenta instanciar dos cadenas diferentes con el mismo valor, solo se instancia una y la otra solo hace referencia a la primera.

Algunas partes de nuestros datos son únicas para cada viñeta individual. Esos se llaman rasgos extrínsecos. Por otro lado, los datos que comparten todas las viñetas, como la textura y la forma antes mencionadas, se denominan características intrínsecas.

Lo que podemos hacer es separar estos rasgos, de modo que todos los rasgos intrínsecos se almacenen en una sola instancia: una clase de peso mosca. Los rasgos extrínsecos están en instancias separadas llamadas clases de contexto. La clase Flyweight normalmente contiene todos los métodos de la clase original y funciona pasándoles una instancia de la clase Contexto.

Para garantizar que el programa funcione según lo previsto, la clase Flyweight debe ser inmutable. De esa forma, si se invoca desde diferentes contextos, no habrá un comportamiento inesperado.

Para uso práctico, a menudo se implementa una fábrica Flyweight. Esta es una clase que, cuando se le pasa un estado intrínseco, verifica si un objeto con ese estado ya existe y lo devuelve si existe. Si no lo hace, crea una instancia de un nuevo objeto y lo devuelve:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BulletContext:
    def __init__(self, x, y, z, velocity):
        self.x = x
        self.y = y
        self.z = z
        self.velocity = velocity
        
 class BulletFlyweight:
    def __init__(self):
        self.asset = '■■►'
        self.bullets = []
        
    def bullet_factory(self, x, y, z, velocity):
        bull = [b for b in self.bullets if b.x==x and b.y==y and b.z==z and b.velocity==velocity]
        if not bull:
            bull = bullet(x,y,z,velocity)
            self.bullets.append(bull)
        else:
            bull = bull[0]
            
        return bull
        
    def print_bullets(self):
        print('Bullets:')
        for bullet in self.bullets:
            print(str(bullet.x)+' '+str(bullet.y)+' '+str(bullet.z)+' '+str(bullet.velocity))

Hemos hecho nuestros contextos y peso mosca. Cada vez que intentamos agregar un nuevo contexto (viñeta) a través de la función bullet_factory(), genera una lista de viñetas existentes que son esencialmente la misma viñeta. Si encontramos esa bala, podemos devolverla. Si no lo hacemos, generamos uno nuevo.

Ahora, con eso en mente, usemos bullet_factory() para instanciar algunas viñetas e imprimir sus valores:

1
2
3
4
5
6
7
bf = BulletFlyweight()

# adding bullets
bf.bullet_factory(1,1,1,1)
bf.bullet_factory(1,2,5,1)

bf.print_bullets()

Esto resulta en:

1
2
3
Bullets:
1 1 1 1
1 2 5 1

Ahora, intentemos agregar más viñetas a través de la fábrica, que ya existen:

1
2
3
# trying to add an existing bullet again
bf.bullet_factory(1,1,1,1)
bf.print_bullets()

Esto resulta en:

1
2
3
Bullets:
1 1 1 1
1 2 5 1

Apoderado

Problema

Un hospital utiliza una pieza de software con una clase PatientFileManager para guardar datos sobre sus pacientes. Sin embargo, dependiendo de su nivel de acceso, es posible que no pueda ver los archivos de algunos pacientes. Después de todo, el derecho a la privacidad prohíbe que el hospital difunda esa información más allá de lo necesario para brindar sus servicios.

Este es solo un ejemplo: el patrón de proxy se puede usar en circunstancias bastante diversas, que incluyen:

  • Manejar el acceso a un objeto que es costoso, como un servidor remoto o una base de datos.
  • Reemplazar objetos cuya inicialización puede ser costosa hasta que realmente se necesiten en un programa, como texturas que ocuparían mucho espacio de RAM o una gran base de datos.
  • Administrar el acceso por motivos de seguridad.

Solución

En nuestro ejemplo del hospital, puede crear otra clase, como un AccessManager, que controla qué usuarios pueden o no pueden interactuar con ciertas funciones de PatientFileManager. El AccessManager es una clase proxy y el usuario se comunica con la clase subyacente a través de él.

Hagamos una clase PatientFileManager:

1
2
3
4
5
6
7
8
9
class PatientFileManager:
    def __init__(self):
        self.__patients = {}
        
    def _add_patient(self, patient_id, data):
        self.__patients[patient_id] = data
        
    def _get_patient(self, patient_id):
        return self.__patients[patient_id]

Ahora, hagamos un proxy para eso:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class AccessManager(PatientFileManager):
    def __init__(self, fm):
        self.fm = fm
    
    def add_patient(self, patient_id, data, password):
        if password == 'sudo':
            self.fm._add_patient(patient_id, data)
        else:
            print("Wrong password.")
            
    def get_patient(self, patient_id, password):
        if password == 'totallytheirdoctor' or password == 'sudo':
            return self.fm._get_patient(patient_id)
        else:
            print("Only their doctor can access this patients data.")

Aquí, tenemos un par de cheques. Si la contraseña proporcionada al proxy es correcta, la instancia de AccessManager puede agregar o recuperar información del paciente. Si la contraseña es incorrecta, no puede.

Ahora, creemos una instancia de AccessManager y agreguemos un paciente:

1
2
3
4
am = AccessManager(PatientFileManager())
am.add_patient('Jessica', ['pneumonia 2020-23-03', 'shortsighted'], 'sudo')

print(am.get_patient('Jessica', 'totallytheirdoctor'))

Esto resulta en:

1
['pneumonia 2020-23-03', 'shortsighted']

Es importante tener en cuenta aquí que Python no tiene verdaderas variables privadas: los guiones bajos son solo una indicación para que otros programadores no toquen las cosas. Entonces, en este caso, implementar un Proxy serviría más para señalar su intención sobre la administración de acceso en lugar de administrar realmente el acceso.

Conclusión

Con esto, todos los patrones de diseño estructural en Python están completamente cubiertos, con ejemplos prácticos.

Muchos programadores comienzan a usarlos como soluciones de sentido común, pero al conocer la motivación y el tipo de problema para usar algunos de estos, es de esperar que pueda comenzar a reconocer situaciones en las que pueden ser útiles y tener un enfoque listo para resolver el problema. . blema. .

Licensed under CC BY-NC-SA 4.0