Imagen destacada del tutorial: Fundamentos de Programación: Patrones de Diseño y Arquitectura de Software
Fundamentos de Programación

Fundamentos de Programación: Patrones de Diseño y Arquitectura de Software

José Elías Romero Guanipa
03 Sep 2025

Aprende patrones de diseño y principios de arquitectura de software con ejemplos prácticos en Python.

patrones diseño arquitectura software solid poo principios +1 más

¡Domina los patrones de diseño y arquitectura de software! En este tutorial avanzado te guiaré paso a paso para que aprendas los principios fundamentales de diseño de software, patrones creacionales, estructurales y de comportamiento, junto con principios SOLID y arquitecturas en capas.

Objetivo: Aprender patrones de diseño fundamentales, principios SOLID, arquitecturas de software y mejores prácticas para crear aplicaciones mantenibles y escalables en Python.

Paso 1: ¿Qué son los patrones de diseño?

Los patrones de diseño son soluciones reutilizables a problemas comunes en el diseño de software. No son código específico, sino conceptos y mejores prácticas que puedes adaptar a diferentes situaciones.

# Analogía: Los patrones son como recetas de cocina para programadores
"""
Singleton → Una única instancia global
Factory → Fábrica de objetos
Observer → Sistema de notificaciones
Strategy → Algoritmos intercambiables
Decorator → Añadir funcionalidad dinámicamente
Adapter → Conectar interfaces incompatibles
"""

Paso 2: Patrones creacionales

Singleton - una única instancia

class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialize()
        return cls._instance

    def _initialize(self):
        print("Inicializando conexión a base de datos...")
        self.connection_count = 0

    def connect(self):
        self.connection_count += 1
        return f"Conexión #{self.connection_count} establecida"

# Uso: Siempre obtenemos la misma instancia
db1 = DatabaseConnection()
db2 = DatabaseConnection()

print(db1 is db2)  # True - misma instancia
print(db1.connect())
print(db2.connect())

Factory Method - creación flexible

from abc import ABC, abstractmethod

class Documento(ABC):
    @abstractmethod
    def abrir(self):
        pass

class PDF(Documento):
    def abrir(self):
        return "Abriendo documento PDF con Adobe Reader"

class Word(Documento):
    def abrir(self):
        return "Abriendo documento Word con Microsoft Word"

class DocumentoFactory:
    @staticmethod
    def crear_documento(tipo, *args):
        if tipo == "pdf":
            return PDF(*args)
        elif tipo == "word":
            return Word(*args)
        else:
            raise ValueError(f"Tipo de documento no soportado: {tipo}")

# Uso de la fábrica
factory = DocumentoFactory()
doc1 = factory.crear_documento("pdf")
doc2 = factory.crear_documento("word")

print(doc1.abrir())
print(doc2.abrir())

Paso 3: Patrones estructurales

Adapter - conectar interfaces incompatibles

# Sistema legacy que queremos adaptar
class SistemaLegacy:
    def obtener_datos_legacy(self):
        return {"nombre_completo": "Juan Pérez", "edad_años": 30, "correo_e": "[email protected]"}

# Nueva interfaz que esperamos
class NuevoSistema:
    def obtener_datos(self):
        return {"firstName": "Ana", "lastName": "García", "age": 25, "email": "[email protected]"}

# Adapter que convierte la interfaz legacy a la nueva
class LegacyAdapter:
    def __init__(self, legacy_system):
        self.legacy_system = legacy_system

    def obtener_datos(self):
        datos_legacy = self.legacy_system.obtener_datos_legacy()
        return {
            "firstName": datos_legacy["nombre_completo"].split()[0],
            "lastName": datos_legacy["nombre_completo"].split()[1],
            "age": datos_legacy["edad_años"],
            "email": datos_legacy["correo_e"]
        }

# Uso del adapter
legacy = SistemaLegacy()
adapter = LegacyAdapter(legacy)
nuevo_sistema = NuevoSistema()

print("Datos legacy adaptados:", adapter.obtener_datos())
print("Datos nuevos:", nuevo_sistema.obtener_datos())

Decorator - añadir funcionalidad dinámicamente

from abc import ABC, abstractmethod

class Notificador(ABC):
    @abstractmethod
    def enviar(self, mensaje):
        pass

class NotificadorBase(Notificador):
    def enviar(self, mensaje):
        return f"Enviando mensaje base: {mensaje}"

class DecoratorNotificador(Notificador):
    def __init__(self, notificador):
        self.notificador = notificador

    def enviar(self, mensaje):
        return self.notificador.enviar(mensaje)

class NotificadorFacebook(DecoratorNotificador):
    def enviar(self, mensaje):
        base_result = super().enviar(mensaje)
        return f"{base_result} + via Facebook"

class NotificadorSlack(DecoratorNotificador):
    def enviar(self, mensaje):
        base_result = super().enviar(mensaje)
        return f"{base_result} + via Slack"

class NotificadorSMS(DecoratorNotificador):
    def enviar(self, mensaje):
        base_result = super().enviar(mensaje)
        return f"{base_result} + via SMS"

# Uso: Podemos combinar decoradores dinámicamente
notificador = NotificadorBase()
notificador = NotificadorFacebook(notificador)
notificador = NotificadorSlack(notificador)
notificador = NotificadorSMS(notificador)

print(notificador.enviar("¡Hola Mundo!"))

Paso 4: Patrones de comportamiento

Observer - sistema de notificaciones

class Observable:
    def __init__(self):
        self._observers = []

    def agregar_observer(self, observer):
        self._observers.append(observer)

    def eliminar_observer(self, observer):
        self._observers.remove(observer)

    def notificar(self, *args, **kwargs):
        for observer in self._observers:
            observer.actualizar(*args, **kwargs)

class Observer(ABC):
    @abstractmethod
    def actualizar(self, *args, **kwargs):
        pass

# Implementaciones concretas
class Usuario(Observer):
    def __init__(self, nombre):
        self.nombre = nombre

    def actualizar(self, mensaje):
        print(f"{self.nombre} recibió: {mensaje}")

class SistemaLogging(Observer):
    def actualizar(self, mensaje):
        print(f"[LOG] Evento: {mensaje}")

# Uso del patrón Observer
sistema_notificaciones = Observable()

usuario1 = Usuario("Ana")
usuario2 = Usuario("Carlos")
logging = SistemaLogging()

sistema_notificaciones.agregar_observer(usuario1)
sistema_notificaciones.agregar_observer(usuario2)
sistema_notificaciones.agregar_observer(logging)

sistema_notificaciones.notificar("¡Nueva actualización disponible!")

Strategy - algoritmos intercambiables

from abc import ABC, abstractmethod

class EstrategiaPago(ABC):
    @abstractmethod
    def pagar(self, cantidad):
        pass

class PagoTarjeta(EstrategiaPago):
    def pagar(self, cantidad):
        return f"Pagando ${cantidad} con tarjeta de crédito"

class PagoPayPal(EstrategiaPago):
    def pagar(self, cantidad):
        return f"Pagando ${cantidad} con PayPal"

class PagoBitcoin(EstrategiaPago):
    def pagar(self, cantidad):
        return f"Pagando ${cantidad} con Bitcoin"

class CarritoCompra:
    def __init__(self):
        self.items = []
        self.estrategia_pago = None

    def agregar_item(self, item, precio):
        self.items.append((item, precio))

    def total(self):
        return sum(precio for _, precio in self.items)

    def set_estrategia_pago(self, estrategia):
        self.estrategia_pago = estrategia

    def checkout(self):
        if not self.estrategia_pago:
            raise ValueError("Estrategia de pago no configurada")

        total = self.total()
        return self.estrategia_pago.pagar(total)

# Uso del patrón Strategy
carrito = CarritoCompra()
carrito.agregar_item("Laptop", 1000)
carrito.agregar_item("Mouse", 50)

# Podemos cambiar la estrategia dinámicamente
carrito.set_estrategia_pago(PagoTarjeta())
print(carrito.checkout())

carrito.set_estrategia_pago(PagoPayPal())
print(carrito.checkout())

carrito.set_estrategia_pago(PagoBitcoin())
print(carrito.checkout())

Paso 5: Principios SOLID

Single Responsibility Principle (SRP)

# ❌ Violación del SRP
class UsuarioMalDiseñado:
    def __init__(self, nombre, email):
        self.nombre = nombre
        self.email = email

    def guardar_en_bd(self):
        # Lógica de persistencia
        print(f"Guardando {self.nombre} en base de datos")

    def enviar_email(self, mensaje):
        # Lógica de notificación
        print(f"Enviando email a {self.email}: {mensaje}")

    def validar_email(self):
        # Lógica de validación
        return "@" in self.email

# ✅ Cumpliendo SRP
class Usuario:
    def __init__(self, nombre, email):
        self.nombre = nombre
        self.email = email

class UsuarioRepository:
    def guardar(self, usuario):
        print(f"Guardando {usuario.nombre} en base de datos")

class EmailService:
    def enviar(self, usuario, mensaje):
        print(f"Enviando email a {usuario.email}: {mensaje}")

class ValidadorUsuario:
    @staticmethod
    def validar_email(usuario):
        return "@" in usuario.email

Open/Closed Principle (OCP)

from abc import ABC, abstractmethod

# ❌ Violación del OCP
class ProcesadorPagos:
    def procesar(self, tipo, cantidad):
        if tipo == "tarjeta":
            return f"Procesando pago con tarjeta: ${cantidad}"
        elif tipo == "paypal":
            return f"Procesando pago con PayPal: ${cantidad}"
        # Debemos modificar la clase para añadir nuevos métodos de pago

# ✅ Cumpliendo OCP
class MetodoPago(ABC):
    @abstractmethod
    def procesar(self, cantidad):
        pass

class TarjetaPago(MetodoPago):
    def procesar(self, cantidad):
        return f"Procesando pago con tarjeta: ${cantidad}"

class PayPalPago(MetodoPago):
    def procesar(self, cantidad):
        return f"Procesando pago con PayPal: ${cantidad}"

class BitcoinPago(MetodoPago):
    def procesar(self, cantidad):
        return f"Procesando pago con Bitcoin: ${cantidad}"

class ProcesadorPagosOCP:
    def procesar(self, metodo_pago, cantidad):
        return metodo_pago.procesar(cantidad)

# Podemos añadir nuevos métodos de pago sin modificar ProcesadorPagosOCP

Paso 6: Arquitectura en capas

Arquitectura MVC (Model-View-Controller)

# Modelo
class UsuarioModel:
    def __init__(self):
        self.usuarios = []

    def agregar_usuario(self, nombre, email):
        self.usuarios.append({"nombre": nombre, "email": email})

    def obtener_usuarios(self):
        return self.usuarios

# Vista
class UsuarioView:
    def mostrar_usuarios(self, usuarios):
        print("\n--- Lista de Usuarios ---")
        for usuario in usuarios:
            print(f"Nombre: {usuario['nombre']}, Email: {usuario['email']}")

# Controlador
class UsuarioController:
    def __init__(self):
        self.model = UsuarioModel()
        self.view = UsuarioView()

    def agregar_usuario(self, nombre, email):
        self.model.agregar_usuario(nombre, email)

    def mostrar_usuarios(self):
        usuarios = self.model.obtener_usuarios()
        self.view.mostrar_usuarios(usuarios)

# Uso del MVC
controller = UsuarioController()
controller.agregar_usuario("Ana", "[email protected]")
controller.agregar_usuario("Carlos", "[email protected]")
controller.mostrar_usuarios()

Arquitectura hexagonal (ports and adapters)

# Núcleo de la aplicación (sin dependencias externas)
class ServicioUsuarios:
    def __init__(self, repositorio):
        self.repositorio = repositorio

    def registrar_usuario(self, nombre, email):
        # Lógica de negocio
        if not email or "@" not in email:
            raise ValueError("Email inválido")

        usuario = {"nombre": nombre, "email": email}
        return self.repositorio.guardar(usuario)

# Puerto (interface)
class RepositorioUsuarios(ABC):
    @abstractmethod
    def guardar(self, usuario):
        pass

# Adaptadores
class RepositorioMemoria(RepositorioUsuarios):
    def __init__(self):
        self.usuarios = []

    def guardar(self, usuario):
        self.usuarios.append(usuario)
        return usuario

class RepositorioBaseDatos(RepositorioUsuarios):
    def guardar(self, usuario):
        # Conexión real a base de datos aquí
        print(f"Guardando en BD: {usuario}")
        return usuario

# Podemos cambiar el adaptador sin modificar el núcleo
servicio = ServicioUsuarios(RepositorioMemoria())
usuario = servicio.registrar_usuario("Laura", "[email protected]")
print(f"Usuario registrado: {usuario}")

Paso 7: Testing y calidad de código

Unit testing con unittest

import unittest

class Calculadora:
    def sumar(self, a, b):
        return a + b

    def restar(self, a, b):
        return a - b

    def multiplicar(self, a, b):
        return a * b

    def dividir(self, a, b):
        if b == 0:
            raise ValueError("No se puede dividir por cero")
        return a / b

class TestCalculadora(unittest.TestCase):
    def setUp(self):
        self.calc = Calculadora()

    def test_sumar(self):
        self.assertEqual(self.calc.sumar(2, 3), 5)
        self.assertEqual(self.calc.sumar(-1, 1), 0)

    def test_restar(self):
        self.assertEqual(self.calc.restar(5, 3), 2)
        self.assertEqual(self.calc.restar(0, 5), -5)

    def test_multiplicar(self):
        self.assertEqual(self.calc.multiplicar(3, 4), 12)
        self.assertEqual(self.calc.multiplicar(0, 5), 0)

    def test_dividir(self):
        self.assertEqual(self.calc.dividir(10, 2), 5)
        self.assertAlmostEqual(self.calc.dividir(1, 3), 0.333, places=3)

        with self.assertRaises(ValueError):
            self.calc.dividir(5, 0)

if __name__ == "__main__":
    unittest.main()

Test-Driven Development (TDD)

# Ejemplo de TDD: Desarrollamos una función usando tests primero

# Paso 1: Escribimos el test (fallará)
def test_contar_vocales():
    assert contar_vocales("hello") == 2
    assert contar_vocales("WORLD") == 1
    assert contar_vocales("") == 0
    assert contar_vocales("aeiou") == 5
    print("Todos los tests pasaron!")

# Paso 2: Implementamos la función mínima para que el test pase
def contar_vocales(texto):
    vocales = "aeiouAEIOU"
    return sum(1 for char in texto if char in vocales)

# Paso 3: Ejecutamos el test
try:
    test_contar_vocales()
    print("✅ Tests pasaron exitosamente!")
except AssertionError as e:
    print(f"❌ Test falló: {e}")

Paso 8: Proyecto completo - sistema de e-commerce con patrones

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List

@dataclass
class Producto:
    id: int
    nombre: str
    precio: float
    stock: int

class ObservadorCarrito(ABC):
    @abstractmethod
    def actualizar(self, carrito):
        pass

class CarritoCompra:
    def __init__(self):
        self.items = []
        self.observadores = []

    def agregar_observador(self, observador):
        self.observadores.append(observador)

    def notificar_observadores(self):
        for observador in self.observadores:
            observador.actualizar(self)

    def agregar_producto(self, producto, cantidad=1):
        if producto.stock >= cantidad:
            self.items.append({"producto": producto, "cantidad": cantidad})
            producto.stock -= cantidad
            self.notificar_observadores()
        else:
            raise ValueError("Stock insuficiente")

    def calcular_total(self):
        return sum(item["producto"].precio * item["cantidad"] for item in self.items)

    def vaciar(self):
        for item in self.items:
            item["producto"].stock += item["cantidad"]
        self.items = []
        self.notificar_observadores()

class ObservadorStock(ObservadorCarrito):
    def actualizar(self, carrito):
        print("🔄 Actualizando niveles de stock...")

class ObservadorTotal(ObservadorCarrito):
    def actualizar(self, carrito):
        total = carrito.calcular_total()
        print(f"💰 Total actual del carrito: ${total:.2f}")

# Factory para crear productos
class ProductoFactory:
    @staticmethod
    def crear_producto(tipo, *args):
        if tipo == "electronico":
            return Producto(*args, stock=50)
        elif tipo == "ropa":
            return Producto(*args, stock=100)
        elif tipo == "libro":
            return Producto(*args, stock=200)
        else:
            raise ValueError("Tipo de producto no válido")

# Uso del sistema
factory = ProductoFactory()

# Crear productos
laptop = factory.crear_producto("electronico", 1, "Laptop Gaming", 1200.00)
camiseta = factory.crear_producto("ropa", 2, "Camiseta Algodón", 25.00)
libro = factory.crear_producto("libro", 3, "Clean Code", 35.00)

# Configurar carrito con observadores
carrito = CarritoCompra()
carrito.agregar_observador(ObservadorStock())
carrito.agregar_observador(ObservadorTotal())

# Operaciones
carrito.agregar_producto(laptop, 1)
carrito.agregar_producto(camiseta, 2)
carrito.agregar_producto(libro, 1)

print(f"Stock laptop después de compra: {laptop.stock}")
print(f"Stock camiseta después de compra: {camiseta.stock}")

# Strategy para descuentos
class EstrategiaDescuento(ABC):
    @abstractmethod
    def aplicar_descuento(self, total):
        pass

class DescuentoPorcentaje(EstrategiaDescuento):
    def __init__(self, porcentaje):
        self.porcentaje = porcentaje

    def aplicar_descuento(self, total):
        return total * (1 - self.porcentaje / 100)

class DescuentoFijo(EstrategiaDescuento):
    def __init__(self, cantidad):
        self.cantidad = cantidad

    def aplicar_descuento(self, total):
        return max(0, total - self.cantidad)

# Aplicar descuento
descuento = DescuentoPorcentaje(10)  # 10% de descuento
total_con_descuento = descuento.aplicar_descuento(carrito.calcular_total())
print(f"Total con descuento: ${total_con_descuento:.2f}")

Paso 9: Ejercicios de práctica

# Ejercicio 1: Implementar el patrón Command
class Comando(ABC):
    @abstractmethod
    def ejecutar(self):
        pass

    @abstractmethod
    def deshacer(self):
        pass

class ComandoEncenderLuz(Comando):
    def __init__(self, luz):
        self.luz = luz

    def ejecutar(self):
        self.luz.encender()

    def deshacer(self):
        self.luz.apagar()

class Luz:
    def __init__(self, nombre):
        self.nombre = nombre
        self.encendida = False

    def encender(self):
        self.encendida = True
        print(f"{self.nombre}: Luz encendida")

    def apagar(self):
        self.encendida = False
        print(f"{self.nombre}: Luz apagada")

# Ejercicio 2: Implementar el patrón State
class EstadoDocumento(ABC):
    @abstractmethod
    def publicar(self, documento):
        pass

    @abstractmethod
    def renderizar(self, documento):
        pass

class EstadoBorrador(EstadoDocumento):
    def publicar(self, documento):
        documento.estado = EstadoPublicado()
        print("Documento publicado desde borrador")

    def renderizar(self, documento):
        print("Renderizando documento en modo borrador")

class EstadoPublicado(EstadoDocumento):
    def publicar(self, documento):
        print("El documento ya está publicado")

    def renderizar(self, documento):
        print("Renderizando documento en modo público")

class Documento:
    def __init__(self, contenido):
        self.contenido = contenido
        self.estado = EstadoBorrador()

    def publicar(self):
        self.estado.publicar(self)

    def renderizar(self):
        self.estado.renderizar(self)

# Probamos el patrón State
doc = Documento("Contenido del documento")
doc.renderizar()
doc.publicar()
doc.renderizar()
doc.publicar()

Paso 10: Próximos pasos en patrones de diseño

Temas para profundizar:

  • Patrones avanzados: Command, State, Template Method
  • Patrones de arquitectura: Microservicios, CQRS, Event Sourcing
  • Testing avanzado: Mocks, stubs, integration tests
  • DevOps y CI/CD: Automatización de despliegues

Paso 11: Recursos y herramientas

Recursos para aprender más:

  • Design Patterns: Gang of Four book
  • Clean Architecture: Robert C. Martin
  • Refactoring: Martin Fowler
  • SOLID Principles: Uncle Bob

Plataformas de práctica:

  • Refactoring Guru: Patrones de diseño interactivos
  • SourceMaking: Ejemplos de patrones
  • Pluralsight: Cursos avanzados

Herramientas de análisis:

  • pylint: Análisis estático de código
  • black: Formateador automático
  • mypy: Chequeo de tipos estático
  • pytest: Framework de testing avanzado

Conclusión

¡Felicidades! Has dominado los fundamentos de los patrones de diseño y arquitectura de software. Practica estos conceptos en proyectos reales y aplica los principios SOLID para crear software mantenible.

Para más tutoriales sobre patrones de diseño avanzados y arquitectura de software, visita nuestra sección de tutoriales.


¡Con estos conocimientos ya puedes crear aplicaciones bien estructuradas y escalables!


💡 Tip Importante

📝 Mejores Prácticas en Patrones de Diseño y Arquitectura

Para aplicar efectivamente los patrones de diseño y principios de arquitectura, considera estos consejos esenciales:

  • Elige el patrón correcto: Estudia el problema antes de seleccionar un patrón
  • No abuses de los patrones: Usa patrones solo cuando realmente solucionen un problema
  • Mantén la simplicidad: Un patrón simple es mejor que uno complejo innecesario
  • Documenta tus decisiones: Explica por qué elegiste ciertos patrones en tu código
  • Refactoriza gradualmente: Mejora la arquitectura de tu código con el tiempo
  • Aprende de ejemplos reales: Estudia código de proyectos open source exitosos
  • Combina patrones: Los patrones funcionan mejor cuando se usan juntos
  • Prueba tus diseños: Asegúrate de que tus patrones funcionen correctamente

📚 Documentación: Revisa la documentación completa de patrones de diseño en Refactoring Guru y principios SOLID en Clean Code

¡Estos consejos te ayudarán a crear software bien diseñado y mantenible!

Comentarios

Comentarios

Inicia sesión para dejar un comentario.

No hay comentarios aún

Sé el primero en comentar este tutorial.

Tutoriales Relacionados

Descubre más tutoriales relacionados que podrían ser de tu interés

Imagen destacada del tutorial relacionado: Fundamentos de Programación: Algoritmos y Pensamiento Lógico
Fundamentos de Programación

Fundamentos de Programación: Algoritmos y Pensamiento Lógico

Desarrolla el pensamiento algorítmico y aprende algoritmos básicos con ejemplos prácticos en Python.

José Elías Romero Guanipa
03 Sep 2025
Imagen destacada del tutorial relacionado: Fundamentos de Programación: Estructuras de Datos y Algoritmos Eficientes
Fundamentos de Programación

Fundamentos de Programación: Estructuras de Datos y Algoritmos Eficientes

Aprende estructuras de datos y algoritmos eficientes con ejemplos prácticos en Python.

José Elías Romero Guanipa
03 Sep 2025
Imagen destacada del tutorial relacionado: Fundamentos de Programación: Programación Orientada a Objetos
Fundamentos de Programación

Fundamentos de Programación: Programación Orientada a Objetos

Aprende los principios de la programación orientada a objetos con ejemplos prácticos en Python.

José Elías Romero Guanipa
03 Sep 2025
Imagen destacada del tutorial relacionado: Fundamentos de Programación: Tu Primer Paso en el Mundo del Código
Fundamentos de Programación

Fundamentos de Programación: Tu Primer Paso en el Mundo del Código

Aprende los fundamentos de la programación desde cero. Variables, funciones, bucles y más con ejemplos prácticos.

José Elías Romero Guanipa
03 Sep 2025
Foto de perfil del autor José Elías Romero Guanipa
José Elías Romero Guanipa
Autor

🌟 Nube de Etiquetas

Descubre temas populares en nuestros tutoriales

python
python 12 tutoriales
ciencia de datos
ciencia de datos 8 tutoriales
pandas
pandas 5 tutoriales
bases de datos
bases de datos 4 tutoriales
dataframe
dataframe 4 tutoriales
principiante
principiante 3 tutoriales
patrones diseño
patrones diseño 3 tutoriales
poo
poo 3 tutoriales
machine learning
machine learning 3 tutoriales
rendimiento
rendimiento 3 tutoriales
mysql
mysql 3 tutoriales
postgresql
postgresql 3 tutoriales
analisis de datos
analisis de datos 3 tutoriales
algoritmos
algoritmos 2 tutoriales
estructuras datos
estructuras datos 2 tutoriales
variables
variables 2 tutoriales
funciones
funciones 2 tutoriales
colaboracion
colaboracion 2 tutoriales
tutorial python
tutorial python 2 tutoriales
json
json 2 tutoriales
csv
csv 2 tutoriales
datetime
datetime 2 tutoriales
metaclasses
metaclasses 2 tutoriales
descriptores
descriptores 2 tutoriales
async await
async await 2 tutoriales

Las etiquetas más grandes y brillantes aparecen en más tutoriales

logo logo

©2024 ViveBTC