Visión general
Contenido
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 .png
archivo 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 ( SvgImage
clase) 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:
Te puede interesar:Ordenar por cubos en Pythonclass 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_png
obras para nuestra graphic_draw()
función. Sin embargo, pasar un regular_svg
no funciona. Al adaptar el regular_svg
objeto, podemos usar la forma adaptada del mismo tal como usaríamos una .png
imagen:
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 .png
y .svg
las 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
format
campo 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 tower
podría extenderse en una watchtower
, lighthouse
y 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_watchtower
extendería limestone_tower
y agregaría detalles de una torre de vigilancia o extendería watchtower
y 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 Building
y Material
.
Querremos tener un puente entre todas las Building
subclases y todas las Material
subclases 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 Building
clase contendrá Material
como 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 Building
clase:
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 CobblestoneMill
o WoodenTower
. En su lugar, podemos crear una instancia de Mill
o Tower
y 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 item
clase 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 Box
sí también es un Item
y podemos agregar una Box
instancia dentro de una Box
instancia. 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. .
Te puede interesar:PNL simple en Python con TextBlob: tokenizaciónInicialmente, podría pensar en usar la herencia para resolver esto. Después de todo, si usted tiene basic_player
, puede heredar blazing_player
, bouncy_player
y bowman_player
de 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 BasePlayer
clase, 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 BouncyPlayer
caracterí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 player
está utilizando un ataque de fuego, así como un doble salto. Estamos decorando el player
con 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.
Te puede interesar:Reemplazar las apariciones de una subcadena en una cadena con PythonSu 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:
Te puede interesar:Detección facial en Python con OpenCVclass 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 PatientFileManager
clase 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 AccessManager
es una clase de proxy y las comunica de usuario con la clase subyacente a través de él.
Hagamos una PatientFileManager
clase:
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 AccessManager
instancia puede agregar o recuperar información del paciente. Si la contraseña es incorrecta, no puede.
Ahora, creemos una instancia AccessManager
y 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.
Te puede interesar:Estimación de la densidad del kernel en Python usando Scikit-Learn