Logotipo de Zephyrnet

Patrones de diseño estructural en Python

Fecha:


General

Este es el segundo artículo de una serie corta dedicada a Patrones de diseño en Python.

Patrones de diseño estructural

Patrones de diseño estructural se usan 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 en capas legible, 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 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

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. Tiene una implementación completa para dibujar, por ejemplo, un .png archivo a la pantalla.

Por 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 para gráficos vectoriales .

Como resultado, hay una biblioteca para trabajar con gráficos vectoriales que puede usar en lugar de implementar usted mismo toda esa funcionalidad completamente nueva. 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 un adaptador clase. Al igual que los adaptadores del mundo real, nuestra clase tomará el recurso disponible externamente (SvgImage clase) y convertirlo en una salida que nos convenga.

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

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

Adaptador de objetos

An 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 vamos a probar 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()

Pasando el regular_png funciona bien para nuestro graphic_draw() función. Sin embargo, pasando un regular_svg no funciona Al adaptar el regular_svg objeto, podemos usar la forma adaptada de la misma manera que usaríamos un .png imagen:

drawing png
drawing rasterized svg

No hay necesidad de cambiar nada dentro de nuestro graphic_draw() función. Funciona igual que antes. Nosotros solo adaptado la entrada para adaptarse a la función ya existente.

Adaptador de clase

Adaptadores de clase solo se puede implementar en idiomas compatibles 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 verificar esto, presentamos 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, probémoslo en ambos .png y .svg 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()

La ejecución de este código da como resultado:

drawing png
drawing rasterized svg

¿Adaptador de objeto o clase?

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

  • El proyecto Composición sobre el principio de herencia asegurando un acoplamiento flojo. En el ejemplo anterior, el supuesto format el campo no tiene que existir para que funcione el adaptador de objetos, mientras que es necesario para el adaptador de clase.
  • Complejidad añadida que puede conducir a problemas que acompañan a la herencia múltiple.

Puente

Problema

Una clase grande puede violar El principio de responsabilidad única y puede necesitar 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, imagine que tenemos una estructura de clase que incluye edificios medievales. Tenemos una wall, tower, stable, mill, house, armory, etc. Ahora queríamos diferenciarlos en función de los materiales de los que están hechos. Podríamos derivar cada clase y hacer straw_wall, log_wall, cobblestone_wall, limestone_watchtower, etc ...

Además, una tower podría extenderse a un watchtower, lighthousey castle_tower.

estructura de clase mala

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 bastante de código repetido.

Por otra parte, sería limestone_watchtower ampliar limestone_tower y agregar detalles de una torre de vigilancia o extender watchtower y agregar detalles del material?

Solución

Para evitar esto, sacaremos la información fundamental y haremos que sea un terreno común sobre el cual construiremos variaciones. En nuestro caso, separaremos una jerarquía de clases para un Building y Material.

Querremos tener un puente entre todos Building subclases y todo Material subclases para que podamos generar variaciones de ellas, sin tener que definirlas como clases separadas. Como un material puede usarse en muchas cosas, el Building la 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 un 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 un CobblestoneMill or WoodenTower clases En cambio, podemos instanciar un Mill or 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 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 los 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 simplemente ejecutar un bucle, porque la estructura de cada cuadro es irregular. Puede recorrer los elementos dentro, 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 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 caja dentro. Una buena manera de lidiar con una estructura como esta es tener el objeto directamente arriba controlando el comportamiento de los que están debajo de él.

El proyecto Compuesto patrón de diseño se utiliza para componer estructuras con forma de árbol y tratan 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 return_price() en una caja, recorre su contenido y suma sus precios (también calculados llamando a su return_price()), y si tiene un artículo, simplemente devuelve su precio.

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

Definiremos un resumen item clase, que todos nuestros elementos específicos heredan de:

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 proyecto Box en sí es un Item también y podemos agregar un Box instancia dentro de un Box ejemplo. Vamos a instanciar algunos elementos y ponerlos 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 daría como resultado:

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 a mitad de la batalla desde un grupo aleatorio.

Esos poderes no se pueden simplificar realmente y poner en una lista que se 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_playery bowman_player de eso.

Pero, ¿qué blazing_bouncy_player, bouncy_bowman_player, blazing_bowman_playery 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 proyecto Patrón decorador se usa para agregar funcionalidad a una clase sin cambiar la clase misma. La idea es crear un contenedor que se ajuste a la misma interfaz que la clase que estamos ajustando, 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 manera, podemos decorar un objeto tantas veces como queramos, sin cambiar 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 un BasePlayer clase, con un 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 un BasePlayer:

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

Ejecutar este código devolvería:

attacking jumping

Ahora, envuélvelo con otra clase que maneje estos comandos de manera diferente:

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

Esto devolverí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 el player está usando un ataque de fuego, así como como doble salto. Estamos decorando el player con diferentes clases Decorámoslo 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 simulando un fenómeno, quizás un concepto evolutivo como el equilibrio entre diferentes estrategias. Usted está a cargo del back-end y tiene que programar qué hacen las muestras 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 hace que su sistema complejo sea accesible para alguien que pueda conocer poco de Game Theory y menos sobre su implementación particular de algún problema?

Solución

El proyecto Patrón de fachada pide un fachada de su implementación. Las personas no necesitan saber todo sobre la implementación subyacente. Puede crear una gran clase que gestione completamente su subsistema complejo y solo proporcione las funcionalidades que probablemente necesitará su usuario.

En el caso de su colega, probablemente desearían 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 tiene que conocer 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, dar este código a nuestro colega requerirá que se familiaricen con el funcionamiento interno antes de intentar visualizar a los animales. En cambio, pintemos una fachada sobre ella y les demos un par de funciones convenientes 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 llamando iterate() y viendo lo que le pasa a 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. Sus viñetas tienen información única, como sus coordenadas y velocidad, pero también tienen información compartida, 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 al mismo tiempo (y no vamos a guardar 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 la memoria caché y hacer que todas las viñetas compartan esa textura única, 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 alberca/cache y solo tira de allí.

Solución

El proyecto Patrón de peso mosca requiere un grupo común cuando pueden existir muchas instancias de un objeto con el mismo valor. Una implementación famosa es el Java String Pool, donde si intentas 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 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 todos los rasgos intrínsecos se almacenen en una sola instancia: un Clase de peso mosca. 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 al pasarles una instancia del Clase de contexto.

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

Para uso práctico, a menudo se implementa una fábrica de Flyweight. Esta es una clase que, cuando pasa un estado intrínseco, comprueba si ya existe un objeto con ese estado y lo devuelve si es así. Si no lo hace, 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 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 el bullet_factory() para instanciar 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 una pieza de software con un 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 proporcionar sus servicios.

Este es solo un ejemplo: el patrón de proxy se puede usar en circunstancias muy 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 requerirían mucho espacio RAM o una gran base de datos
  • Administrar el acceso por motivos de seguridad

Solución

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

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

Ahora, vamos a instanciar un AccessManager y agrega 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 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, la implementación de un Proxy serviría más para indicar su intención sobre la administración del acceso en lugar de realmente administrar el acceso.

Conclusión

Con esto, todo 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 un 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. .

Fuente: https://stackabuse.com/structural-design-patterns-in-python/

punto_img

Información más reciente

punto_img