Patrones de diseño estructural en Python

P

Visión general

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 múltiples clases en estructuras de trabajo más grandes.

A veces, las interfaces para trabajar con varios 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 todos los elementos parecen necesarios .

Son muy útiles para crear código legible, mantenible y en capas, 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
  • Puente
  • Compuesto
  • Decorador
  • Fachada
  • Peso mosca
  • Apoderado

Adaptador

En el mundo real, puede usar un adaptador para conectar cargadores a diferentes enchufes cuando viaje 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 recibió su nombre porque su propósito es el mismo: adaptar una entrada a una salida predeterminada diferente.

Problema

Supongamos que está trabajando en un software de visualización de imágenes y, hasta ahora, sus clientes solo querían mostrar imágenes rasterizadas . Tiene una implementación completa para dibujar, digamos, un .pngarchivo en la pantalla.

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

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 expandir su público objetivo ofreciendo más funcionalidad, 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 draw()método):

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 ( SvgImageclase) y lo convertirá en una salida que nos convenga.

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

Nuevamente, por simplicidad, solo 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:

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:

regular_png = PngImage("some data")
regular_png.draw()

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

Pasar bien las regular_pngobras para nuestra graphic_draw()función. Sin embargo, pasar un regular_svgno funciona. Al adaptar el regular_svgobjeto, podemos usar la forma adaptada del mismo tal como usaríamos una .pngimagen:

drawing png
drawing rasterized svg

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

Adaptador de clase

Los adaptadores de clase solo se pueden implementar en lenguajes que admitan herencia múltiple. Heredan tanto nuestra clase como la clase externa, heredando así 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:

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:

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, la prueba de dejarlo salir en tanto .pngy .svglas imágenes:

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:

drawing png
drawing rasterized svg

¿Adaptador de objeto o clase?

En general, debería preferir utilizar 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 garantiza un acoplamiento flojo. En el ejemplo anterior, el formatcampo supuesto no tiene que existir para que el adaptador de objetos funcione, mientras que es necesario para el adaptador de clases.
  • 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 una wall, tower, stable, mill, house, armory, etc. Ahora querían diferenciarlos basado en materiales que están hechos de. Podríamos derivar todas las clases y hacer straw_wall, log_wall, cobblestone_wall, limestone_watchtower, etc …

Además, una towerpodría extenderse en una watchtower, lighthousey castle_tower.

Pero esto resultaría en un crecimiento exponencial del número de clases si continuamos agregando atributos de manera similar. Además, estas clases tendrían mucho código repetido.

Además, ¿ limestone_watchtowerextendería limestone_towery agregaría detalles de una torre de vigilancia o extendería watchtowery agregaría detalles de material?

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 a Buildingy Material.

Querremos tener un puente entre todas las Buildingsubclases y todas las Materialsubclases 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 Buildingclase contendrá Materialcomo uno de sus campos:

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 Buildingclase:

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 clases CobblestoneMillo WoodenTower. En su lugar, podemos crear una instancia de Millo Towery asignarle cualquier material que nos gustaría:

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:

cobblestone mill Hilltop Mill
wooden tower Abandoned Sentry

Compuesto

Problema

Imagine que está ejecutando un servicio de entrega y los proveedores envían cajas grandes 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 un fastidio.

Esto no es tan simple como ejecutar un bucle, porque la estructura de cada caja es irregular. Puede recorrer los elementos del interior, claro, pero ¿qué sucede si una caja contiene otra caja con elementos dentro? ¿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 en forma de árbol. Tienes la caja más grande, en la parte superior. Y luego tienes artículos más pequeños o dentro de una caja. Una buena forma de lidiar con una estructura como esta es tener el objeto directamente arriba controlando 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 caja contenga una lista de su contenido y asegurarnos de que todas las cajas y elementos tengan una función – return_price(). Si llama return_price()a una caja, recorre su contenido y suma sus precios (también se calcula llamando a sus return_price()), y si tiene un artículo, simplemente devuelve su precio.

Hemos creado una situación similar a la recursividad en la que resolvemos un gran problema dividiéndolo en problemas más pequeños e invocando la misma operación sobre ellos. En cierto sentido, estamos haciendo una búsqueda en profundidad a través de la jerarquía de objetos.

Definiremos una itemclase abstracta , de la que todos nuestros elementos específicos heredan:

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:

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 en Boxsí también es un Itemy podemos agregar una Boxinstancia dentro de una Boxinstancia. Creemos una instancia de algunos elementos y colóquelos en una caja antes de obtener su valor:

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 resultaría en:

Total price: 220

Decorador

Problema

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

Esos poderes realmente no se pueden simplificar y poner en una lista en la que puede iterar, 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 usted tiene basic_player, puede heredar blazing_player, bouncy_playery bowman_playerde ella.

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 de decorador se utiliza para agregar funcionalidad a una clase sin cambiar la clase en sí. La idea es crear una envoltura 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 simplemente agregar algo de su propia funcionalidad encima, 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 poco la clase original. Sigamos adelante y definamos un PlayerDecorator:

from abc import ABC, abstractmethod

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

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

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 BasePlayer:

player = BasePlayer()
player.handle_input('e')
player.handle_input(' ')

Ejecutar este código devolvería:

attacking 
jumping

Ahora, envuémoslo con otra clase que maneja estos comandos de manera diferente:

player = BlazingPlayer(player)
player.handle_input('e')
player.handle_input(' ')

Esto volvería:

using fire attacking 
jumping

Ahora, agreguemos BouncyPlayercaracterísticas:

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

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

player = BowmanPlayer(player)
player.handle_input('e')
player.handle_input(' ')

Esto devuelve:

with arrows using fire attacking 
double jump

Fachada

Problema

Digamos que estás haciendo una simulación de un fenómeno, quizás 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 compleja estructura subyacente no es muy importante para su colega, solo quieren saber dónde está cada espécimen y cómo se supone que deben verse.

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

Solución

El patrón de fachada requiere una fachada de su implementación. La gente no necesita saber todo sobre la implementación subyacente. Puede crear una clase grande que administre completamente su complejo subsistema y solo proporcione las funcionalidades que su usuario probablemente necesite.

En el caso de su colega, probablemente querrán 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, puede omitir leerlo, ya que el punto es que no es necesario que conozca los detalles para usarlo:

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 cambio, pintemos una fachada sobre él y démosles un par de funciones de conveniencia para iterar la población y acceder a animales individuales desde ella:

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 con las llamadas iterate()y ver qué pasa con la población.

Peso mosca

Problema

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

class Bullet:
    def __init__(self, x, y, z, velocity):
        self.x = x
        self.y = y
        self.z = z
        self.velocity = velocity
        self.asset="■■►"

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

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

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

Solución

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

Algunas partes de nuestros datos son exclusivas de 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 rasgos intrínsecos.

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

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á ningún comportamiento inesperado.

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

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 bullet_factory()función, genera una lista de viñetas existentes que son esencialmente la misma viñeta. Si encontramos una bala así, podemos devolverla. Si no lo hacemos, generamos uno nuevo.

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

bf = BulletFlyweight()

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

bf.print_bullets()

Esto resulta en:

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:

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

Esto resulta en:

Bullets:
1 1 1 1
1 2 5 1

Apoderado

Problema

Un hospital utiliza un software con una PatientFileManagerclase 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 al hospital difundir esa información más allá de lo necesario para que puedan 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
  • Reemplazo de objetos cuya inicialización puede ser costosa hasta que realmente se necesitan en un programa, como texturas que ocuparían mucho espacio de RAM o una gran base de datos
  • Gestionar el acceso por motivos de seguridad

Solución

En nuestro ejemplo de hospital, puede hacer otra clase, como an AccessManager, que controla qué usuarios pueden o no pueden interactuar con ciertas características de PatientFileManager. El AccessManageres una clase de proxy y las comunica de usuario con la clase subyacente a través de él.

Hagamos una PatientFileManagerclase:

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:

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 AccessManagerinstancia puede agregar o recuperar información del paciente. Si la contraseña es incorrecta, no puede.

Ahora, creemos una instancia AccessManagery agreguemos un paciente:

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

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

Esto resulta en:

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

Es importante señalar 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 del acceso que para administrar realmente el acceso.

Conclusión

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

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

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad