Patrón de diseño - Bri0714/Cliente-Servidor GitHub Wiki

Patrón de Diseño Modelo-Vista-Controlador (MVC)

Introducción al Patrón MVC

El Modelo-Vista-Controlador (MVC) es un patrón de diseño arquitectónico ampliamente utilizado en el desarrollo de software para organizar el código de manera modular y mejorar su mantenimiento. Su principal objetivo es separar la lógica de negocio, la interfaz de usuario y la gestión de eventos, permitiendo que cada componente sea independiente y reutilizable.

Componentes del MVC

  1. Modelo (Model)

    • Representa los datos y la lógica de negocio de la aplicación.
    • Gestiona el acceso a la base de datos o cualquier otro recurso de datos.
    • No debe contener lógica de presentación.
  2. Vista (View)

    • Se encarga de la presentación de los datos al usuario.
    • Recibe datos del modelo y los muestra de una manera comprensible.
    • No debe contener lógica de negocio.
  3. Controlador (Controller)

    • Actúa como intermediario entre la Vista y el Modelo.
    • Procesa las entradas del usuario y llama a los métodos apropiados del modelo.
    • Actualiza la vista en función de los cambios en el modelo.

Ventajas del Patrón MVC

  • Separación de responsabilidades, lo que facilita la mantenibilidad del código.
  • Reutilización de componentes, permitiendo usar el mismo modelo con diferentes vistas.
  • Facilita las pruebas unitarias, ya que cada componente puede probarse por separado.
  • Mejor organización del código, reduciendo la complejidad y aumentando la escalabilidad.

Implementación del MVC en la Aplicación para consultar tarjetas de crédito

A continuación, se describe cómo se implementó el patrón MVC en la aplicación para consultar tarjetas de crédito para el Servidor.

Modelo: BaseDeDatos.py

import sqlite3
from datetime import datetime

class BaseDeDatos:
    
    # Constructor de la clase
    def __init__(self, db_nombre='banco_universidad.db'):
        try: 
            self.conexion = sqlite3.connect(db_nombre, check_same_thread=False)
            self.cursor = self.conexion.cursor()
            self.crear_tablas()
            #self.precargar_datos() Para que no se vuelvan a replicar los datos cada vez que se inicie el servidor se crea el metodo verificar_y_precargar_datos
            self.verificar_y_precargar_datos()
            print('Conexión exitosa a la base de datos')
        except sqlite3.Error as e:
            print(f'Error en la conexión a la base de datos: {e}')
            
    # Método para Crear tablas
    def crear_tablas(self):
        try:
            # Tabla cliente
            self.cursor.execute(
                """
                CREATE TABLE IF NOT EXISTS cliente(
                    id_cliente INTEGER PRIMARY KEY AUTOINCREMENT,
                    nombre TEXT NOT NULL
                )
                """
            )
            print('Tabla Cliente creada con éxito')

            # Tabla tarjeta
            self.cursor.execute(
                """
                CREATE TABLE IF NOT EXISTS tarjeta(
                    id_tarjeta INTEGER PRIMARY KEY AUTOINCREMENT,
                    id_cliente INTEGER NOT NULL,
                    nombre_banco TEXT NOT NULL,
                    numero_tarjeta TEXT NOT NULL,
                    cupo_total REAL NOT NULL,
                    cupo_disponible REAL NOT NULL,
                    FOREIGN KEY (id_cliente) REFERENCES cliente(id_cliente)
                )
                """
            )
            print('Tabla Tarjeta creada con éxito')

            # Tabla compras
            self.cursor.execute(
                """
                CREATE TABLE IF NOT EXISTS compras(
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    numero_tarjeta TEXT,
                    fecha TEXT,
                    monto REAL,
                    descripcion TEXT,
                    FOREIGN KEY (numero_tarjeta) REFERENCES tarjeta(numero_tarjeta)
                )
                """
            )
            print('Tabla Compras creada con éxito')

            self.conexion.commit()
        except sqlite3.Error as e:
            print(f'Error en la creación de la tabla: {e}')
            
    # Método para verificar y precargar los datos
    def verificar_y_precargar_datos(self):
        try:
            self.cursor.execute("SELECT COUNT(*) FROM cliente")
            resultado = self.cursor.fetchone()
            
            if resultado and resultado[0] == 0:
                self.precargar_datos()
        except sqlite3.Error as e:
            print(f'Error al verificar y precargar datos: {e}')
            
    # Método para precargar los datos
    def precargar_datos(self):
        try:
            self.cursor.executescript(
                """
                -- Insertar clientes
                INSERT INTO cliente (nombre) VALUES
                ('Juan Pérez'),
                ('María Gómez'),
                ('Carlos Rodríguez'),
                ('Laura Sánchez'),
                ('Andrés Fernández'),
                ('Diana Castro'),
                ('Sergio Ramírez'),
                ('Valentina López'),
                ('Felipe Morales'),
                ('Camila Vargas');

                -- Insertar tarjetas con bancos colombianos
                INSERT INTO tarjeta (id_cliente, nombre_banco, numero_tarjeta, cupo_total, cupo_disponible) VALUES
                (1, 'BBVA', '1111-2222-3333-4444', 5000000, 5000000),
                (1, 'Davivienda', '5555-6666-7777-8888', 7000000, 7000000),
                (2, 'Banco Bogotá', '2222-3333-4444-5555', 4000000, 4000000),
                (2, 'Bancolombia', '6666-7777-8888-9999', 6000000, 6000000),
                (3, 'Colpatria', '3333-4444-5555-6666', 3000000, 3000000),
                (3, 'Mastercard', '7777-8888-9999-0000', 8000000, 8000000),
                (4, 'Banco de Occidente', '4444-5555-6666-7777', 5000000, 5000000),
                (4, 'BBVA', '8888-9999-0000-1111', 7000000, 7000000),
                (5, 'Davivienda', '5555-6666-7777-8889', 6000000, 6000000),
                (5, 'Banco Bogotá', '9999-0000-1111-2222', 9000000, 9000000),
                (6, 'Bancolombia', '6666-7777-8888-9998', 5000000, 5000000),
                (6, 'Colpatria', '1111-3333-5555-7777', 7000000, 7000000),
                (7, 'Mastercard', '2222-4444-6666-8888', 4000000, 4000000),
                (7, 'Banco de Occidente', '3333-5555-7777-9999', 8000000, 8000000),
                (8, 'BBVA', '4444-6666-8888-0000', 5000000, 5000000),
                (8, 'Davivienda', '5555-7777-9999-1111', 7000000, 7000000),
                (9, 'Banco Bogotá', '6666-8888-0000-2222', 6000000, 6000000),
                (9, 'Bancolombia', '7777-9999-1111-3333', 9000000, 9000000),
                (10, 'Colpatria', '8888-0000-2222-4444', 5000000, 5000000),
                (10, 'Mastercard', '9999-1111-3333-5555', 7000000, 7000000);

                -- Insertar compras por tarjeta (2 compras por cada tarjeta)
                INSERT INTO compras (numero_tarjeta, fecha, monto, descripcion) VALUES
                ('1111-2222-3333-4444', '2025-02-01', 200000, 'Compra en supermercado'),
                ('1111-2222-3333-4444', '2025-02-05', 300000, 'Pago de servicios'),
                ('5555-6666-7777-8888', '2025-02-02', 400000, 'Compra en tienda de ropa'),
                ('5555-6666-7777-8888', '2025-02-06', 500000, 'Restaurante'),
                ('2222-3333-4444-5555', '2025-02-07', 600000, 'Gasolina'),
                ('2222-3333-4444-5555', '2025-02-10', 200000, 'Compra en línea'),
                ('6666-7777-8888-9999', '2025-02-12', 700000, 'Electrodomésticos'),
                ('6666-7777-8888-9999', '2025-02-15', 300000, 'Pago de internet'),
                ('3333-4444-5555-6666', '2025-02-18', 400000, 'Compra en ferretería'),
                ('3333-4444-5555-6666', '2025-02-20', 150000, 'Cine y entretenimiento'),
                ('7777-8888-9999-0000', '2025-02-21', 800000, 'Compra de tecnología'),
                ('7777-8888-9999-0000', '2025-02-23', 200000, 'Taxi y transporte'),
                ('4444-5555-6666-7777', '2025-02-24', 100000, 'Compra en librería'),
                ('4444-5555-6666-7777', '2025-02-26', 300000, 'Pago de servicios públicos'),
                ('8888-9999-0000-1111', '2025-02-27', 500000, 'Cena en restaurante'),
                ('8888-9999-0000-1111', '2025-02-28', 600000, 'Compra de muebles'),
                ('5555-6666-7777-8889', '2025-03-01', 250000, 'Compra en supermercado'),
                ('5555-6666-7777-8889', '2025-03-03', 350000, 'Pago de gimnasio'),
                ('9999-0000-1111-2222', '2025-03-05', 450000, 'Compra de videojuegos'),
                ('9999-0000-1111-2222', '2025-03-07', 200000, 'Taxi y transporte'),
                ('6666-7777-8888-9998', '2025-03-10', 700000, 'Electrodomésticos'),
                ('6666-7777-8888-9998', '2025-03-12', 500000, 'Pago de internet'),
                ('1111-3333-5555-7777', '2025-03-15', 800000, 'Compra en ferretería'),
                ('1111-3333-5555-7777', '2025-03-17', 300000, 'Cine y entretenimiento'),
                ('2222-4444-6666-8888', '2025-03-18', 500000, 'Compra de tecnología'),
                ('2222-4444-6666-8888', '2025-03-20', 200000, 'Taxi y transporte'),
                ('3333-5555-7777-9999', '2025-03-21', 150000, 'Compra en librería'),
                ('3333-5555-7777-9999', '2025-03-23', 300000, 'Pago de servicios públicos'),
                ('4444-6666-8888-0000', '2025-03-25', 600000, 'Cena en restaurante'),
                ('4444-6666-8888-0000', '2025-03-27', 500000, 'Compra de muebles');

                -- 5 Actualizar el cupo disponible de cada tarjeta
                UPDATE tarjeta
                SET cupo_disponible = cupo_total - (
                    SELECT COALESCE(SUM(monto), 0)
                    FROM compras
                    WHERE compras.numero_tarjeta = tarjeta.numero_tarjeta
                );
                """
            )
            self.conexion.commit()
            print("✅ Datos precargados con éxito")
        except sqlite3.Error as e:
            print(f"❌ Error al precargar datos: {e}")

    # Metodo para obtener los detalles de un cliente
    def obtener_detalle_cliente(self, id_cliente, fecha_inicio=None, fecha_fin=None):
            self.cursor.execute("SELECT nombre FROM cliente WHERE id_cliente = ?", (id_cliente,))
            cliente = self.cursor.fetchone()
            
            self.cursor.execute("SELECT nombre_banco, numero_tarjeta, cupo_total, cupo_disponible FROM tarjeta WHERE id_cliente = ?", (id_cliente,))
            tarjetas = self.cursor.fetchall()
            
            query = """
                    SELECT c.fecha, c.monto, c.descripcion, t.nombre_banco, t.numero_tarjeta
                    FROM compras c
                    JOIN tarjeta t ON c.numero_tarjeta = t.numero_tarjeta
                    WHERE t.id_cliente = ?
                    """
            params = [id_cliente]
            
            if fecha_inicio and fecha_fin:
                query += " AND fecha BETWEEN ? AND ?"
                params.extend([fecha_inicio, fecha_fin])
            
            self.cursor.execute(query, tuple(params))
            compras = self.cursor.fetchall()
            
            #print(cliente,tarjetas,compras)
            return cliente, tarjetas, compras
    
    # Metodo para obtener todos los clientes
    def obtener_clientes(self):
        self.cursor.execute("SELECT * FROM cliente")
        return self.cursor.fetchall()
    

📌 Función en el MVC:

  • Gestiona la conexión a la base de datos SQLite.
  • Crea y estructura las tablas necesarias (cliente, tarjeta, compras).
  • Inserta datos de prueba si la base está vacía.
  • Contiene métodos para obtener detalles de clientes, tarjetas y compras.

Vista: vista.py

import sys
import os

# Agregar el directorio raíz al sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

import socket
import json
import threading
from Controlador.Controlador import Controlador

class ServidorVista:
    #Clase que representa el servidor y maneja múltiples clientes usando hilos.

    def __init__(self, host="localhost", puerto=5000):
        #Inicializa el servidor con los datos básicos."""
        self.host = host
        self.puerto = puerto
        self.servidor = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.controlador = Controlador()  # Instancia del controlador

    def iniciar_servidor(self):
        #Inicia el servidor y espera conexiones de clientes.
        self.servidor.bind((self.host, self.puerto)) # .bind() enlaza el socket a la dirección y puerto especificados
        self.servidor.listen(5) # .listen() pone el socket en modo de escucha para aceptar conexiones
        print(f"Servidor iniciado en {self.host}:{self.puerto}")
        
        while True:
            conexion, _ = self.servidor.accept()
            print("Cliente conectado.")
            hilo = threading.Thread(target=self.manejar_cliente, args=(conexion,))
            hilo.start()  # Iniciar el hilo para manejar al cliente

    def manejar_cliente(self, conexion):
        #Maneja la conexión de un cliente en un hilo separado.
        try:
            datos = json.loads(conexion.recv(1024).decode("utf-8"))
            respuesta = self.controlador.procesar_peticion(datos)  # Procesar petición con el controlador
            conexion.sendall(json.dumps(respuesta).encode("utf-8"))
        except Exception as e:
            conexion.sendall(json.dumps({"error": f"Error del servidor: {e}"}).encode("utf-8"))
        finally:
            conexion.close()

if __name__ == "__main__":
    servidor = ServidorVista()
    servidor.iniciar_servidor()

📌 Función en el MVC:

  • Implementa un servidor basado en sockets para recibir peticiones de clientes.
  • Crea un hilo por cada conexión entrante.
  • Recibe datos en JSON, los envía al controlador y responde al cliente con los resultados procesados.

Controlador: Controlador.py

from Modelo.BaseDeDatos import BaseDeDatos

class Controlador:
    def __init__(self):
        self.db = BaseDeDatos()
    
    def procesar_peticion(self, datos):
        if datos["accion"] == "listar_clientes":
            clientes = self.db.obtener_clientes()
            return {"clientes": [{"id": c[0], "nombre": c[1]} for c in clientes]}
        
        elif datos["accion"] == "detalle_cliente":
            id_cliente = datos.get("id_cliente")
            fecha_inicio = datos.get("fecha_inicio")
            fecha_fin = datos.get("fecha_fin")
            
            cliente, tarjetas, compras = self.db.obtener_detalle_cliente(id_cliente, fecha_inicio, fecha_fin)
            if not cliente:
                return {"error": "Cliente no encontrado"}
            
            return {
                "nombre": cliente[0],
                "num_compras": len(compras),
                "tarjetas": [
                    {
                        "nombre_banco": t[0], 
                        "numero_tarjeta": t[1],
                        "cupo_total": t[2],
                        "cupo_disponible": t[3]
                    } 
                    for t in tarjetas],
                "compras": [
                    {
                        "fecha": c[0], 
                        "monto": c[1], 
                        "descripcion": c[2],
                        "nombre_banco": c[3],
                        "numero_tarjeta": c[4]
                    } 
                    for c in compras]
            }
        
        return {"error": "Acción no reconocida"}

📌 Función en el MVC:

  • Actúa como intermediario entre la vista y el modelo.
  • Recibe peticiones desde la vista y decide qué acción tomar.
  • Consulta a BaseDeDatos y formatea los datos antes de devolverlos a la vista.
  • Soporta acciones como listar_clientes y detalle_cliente.

A continuación, se describe cómo se implementó el patrón MVC en la aplicación para consultar tarjetas de crédito para el Cliente.

Modelo: modelo.py

import socket
import json

class ClienteModelo:
    #Clase encargada de la comunicación con el servidor

    def __init__(self, host="localhost", puerto=5000):
        self.host = host
        self.puerto = puerto

    def enviar_peticion(self, datos):
        """Envía una petición JSON al servidor y recibe la respuesta"""
        try:
            cliente = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            cliente.connect((self.host, self.puerto))
            cliente.sendall(json.dumps(datos).encode("utf-8"))
            respuesta = json.loads(cliente.recv(4096).decode("utf-8"))
            cliente.close()
            return respuesta
        except Exception as e:
            return {"error": f"Error de conexión: {e}"}

    def obtener_clientes(self):
        #Solicita la lista de clientes al servidor
        return self.enviar_peticion({"accion": "listar_clientes"})

    def obtener_detalle_cliente(self, id_cliente, fecha_inicio=None, fecha_fin=None):
        #Solicita detalles de un cliente, incluyendo tarjetas y compras
        datos = {"accion": "detalle_cliente", "id_cliente": id_cliente}
        if fecha_inicio and fecha_fin:
            datos["fecha_inicio"] = fecha_inicio
            datos["fecha_fin"] = fecha_fin
        return self.enviar_peticion(datos)
    

📌 Función en el MVC:

  • Se encarga de la comunicación con el servidor.
  • Usa sockets para enviar peticiones en formato JSON y recibir respuestas.
  • Tiene métodos para solicitar la lista de clientes y obtener detalles de un cliente específico.

Vista: vista.py

import socket
import json

class ClienteModelo:
    #Clase encargada de la comunicación con el servidor

    def __init__(self, host="localhost", puerto=5000):
        self.host = host
        self.puerto = puerto

    def enviar_peticion(self, datos):
        """Envía una petición JSON al servidor y recibe la respuesta"""
        try:
            cliente = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            cliente.connect((self.host, self.puerto))
            cliente.sendall(json.dumps(datos).encode("utf-8"))
            respuesta = json.loads(cliente.recv(4096).decode("utf-8"))
            cliente.close()
            return respuesta
        except Exception as e:
            return {"error": f"Error de conexión: {e}"}

    def obtener_clientes(self):
        #Solicita la lista de clientes al servidor
        return self.enviar_peticion({"accion": "listar_clientes"})

    def obtener_detalle_cliente(self, id_cliente, fecha_inicio=None, fecha_fin=None):
        #Solicita detalles de un cliente, incluyendo tarjetas y compras
        datos = {"accion": "detalle_cliente", "id_cliente": id_cliente}
        if fecha_inicio and fecha_fin:
            datos["fecha_inicio"] = fecha_inicio
            datos["fecha_fin"] = fecha_fin
        return self.enviar_peticion(datos)

📌 Función en el MVC:

  • Es la interfaz gráfica de la aplicación, construida con Flet.
  • Contiene botones, campos de entrada y listas desplegables para interactuar con el usuario.
  • Usa ClienteControlador para solicitar y mostrar datos.
  • Formatea la información recibida del servidor, mostrando los clientes, tarjetas y compras.

Controlador: Controlador.py

from Modelo.modelo import ClienteModelo

class ClienteControlador:
    #Clase que maneja la lógica de la aplicación
    def __init__(self, vista):
        self.modelo = ClienteModelo()
        self.vista = vista

    def cargar_clientes(self):
        #Obtiene la lista de clientes y la envía a la vista
        clientes = self.modelo.obtener_clientes()
        if "clientes" in clientes:
            self.vista.mostrar_clientes(clientes["clientes"])
        else:
            self.vista.mostrar_mensaje("Error al obtener clientes")

    def mostrar_detalle_cliente(self, id_cliente, fecha_inicio=None, fecha_fin=None):
        #Obtiene los detalles del cliente y los envía a la vista
        detalle = self.modelo.obtener_detalle_cliente(id_cliente, fecha_inicio, fecha_fin)
        if "error" in detalle:
            self.vista.mostrar_mensaje(detalle["error"])
        else:
            self.vista.mostrar_detalle(detalle)

📌 Función en el MVC:

  • Actúa como intermediario entre el modelo y la vista.
  • Llama a ClienteModelo para obtener datos del servidor.
  • Procesa los datos y los envía a la vista para que sean mostrados.
  • Maneja la carga de clientes y la consulta de detalles de un cliente.

Punto de entrada: main.py

import flet as ft
from Vista.vista import ClienteVista

def main(page: ft.Page):
    page.title = "Gestión de Clientes"
    ClienteVista(page)

ft.app(target=main)


📌 Función en el MVC:

  • Se encarga de iniciar la aplicación con Flet.
  • Define el título de la ventana y carga la interfaz gráfica ClienteVista.
  • ft.app(target=main) inicia la aplicación en un entorno Flet.