Apariencia
📙 Clase 29 — Arquitectura de una app de escritorio
Fase 6 · Especialización: Apps de escritorio 🎯 ⬅️ Volver al índice de clases
🎯 Qué aprendí
- Las 3 capas de una app profesional: datos ↔ lógica ↔ interfaz (MVC ligero).
- La estructura de carpetas estándar y la regla de dependencias.
- Cómo fluye un clic de principio a fin por las capas.
📖 PARTE TEÓRICA
🏛️ 1. Las 3 capas (MVC ligero)
Todo lo que construiste en las Fases 2–5 apuntaba aquí. Una app seria se divide en:
┌─────────────────────────────────────────────────────┐
│ VISTA (ui.py) │ 🎨 widgets, layouts,
│ "muestra y captura" │ callbacks
├─────────────────────────────────────────────────────┤
│ LÓGICA (logica.py) ← el "controlador/ │ 🧠 validaciones, reglas,
│ "decide" modelo de negocio" │ funciones puras
├─────────────────────────────────────────────────────┤
│ DATOS (datos.py) │ 🗄️ SQLite / JSON / API
│ "guarda y recupera" │
└─────────────────────────────────────────────────────┘
Las flechas de import apuntan SIEMPRE hacia abajo:
ui → logica → datos (jamás al revés)| Capa | Responde a | Puede importar | PROHIBIDO |
|---|---|---|---|
| Vista | "¿cómo se ve y qué tocó el usuario?" | logica, datos | reglas de negocio |
| Lógica | "¿es válido? ¿qué hacemos?" | datos | import ui, tocar widgets |
| Datos | "¿dónde y cómo se guarda?" | — | conocer la lógica o la vista |
📁 2. Estructura de carpetas estándar
mi_app/
├── main.py 🚪 punto de entrada (10 líneas máx)
├── ui/
│ ├── app.py class App(ctk.CTk) — arma la ventana
│ └── paneles.py frames-clase reutilizables (Clase 15)
├── logica/
│ └── contactos.py validar_contacto(), reglas
├── datos/
│ └── bd.py conectar(), CRUD (Clases 30-31)
└── requirements.txt customtkinter==6.0.0 … (pip freeze)python
# main.py — TODO el arranque:
from ui.app import App
if __name__ == "__main__":
App().mainloop()📌 Una carpeta con módulos es un paquete; se importa con puntos:
from ui.app import App. Para apps pequeñas (tus ejercicios), la versión planaui.py / logica.py / datos.pyen la raíz es perfectamente válida — misma regla de capas, menos ceremonias.
🔄 3. La vida de un clic
Un clic en "Guardar" recorre las capas bajando y subiendo:
usuario ── clic ──▶ VISTA callback _guardar(): lee entries
│
▼
LÓGICA validar_contacto(nombre, tel) → (ok, msg)
│ │
│ ok=False │ ok=True
▼ ▼
VISTA DATOS insertar_contacto(con, dict)
muestra msg │
▼
VISTA ◀── vuelve ok → repintar lista + status "✅"🧪 Tip de entrevista: "¿Qué es MVC?" → Separar Modelo (datos/estado), Vista (presentación) y Controlador (coordinación) para poder cambiar/probar cada parte sin romper las otras. En apps de escritorio pequeñas se usa una versión ligera: vista / lógica / datos.
🧪 4. El premio: probar sin abrir ventanas
Como la lógica y los datos no conocen la GUI, se prueban en terminal (¡o con pytest!):
python
# prueba rápida sin ventana:
from logica.contactos import validar_contacto
assert validar_contacto("", "999") == (False, "El nombre está vacío")
assert validar_contacto("Ana", "999")[0] is True
print("lógica OK ✔")💻 PARTE PRÁCTICA — tu misión
Toma tu To-Do (ya refactorizado como clase en la Clase 18) y divídelo en 3 archivos. Pasos:
- Crea
logica.pyy mueve ahívalidar()y cualquier regla (funciones puras). - Crea
datos.pyy mueve ahí_cargar()/_guardar()(JSON por ahora; en la Clase 30 lo cambiarás por SQLite sin tocar las otras capas — esa es la gracia). app.py(la vista) queda solo con widgets y callbacks que llaman a las otras dos.main.pycon el guardián y 3 líneas.- Checklist: ¿
logica.pyimportacustomtkinter? ❌ No debe. ¿datos.pysabe qué es un label? ❌ Jamás.
🔐 Solución de referencia (ábrela solo DESPUÉS de intentarlo)
Verificada en el .venv: las capas logica/datos pasan sus pruebas sin GUI, y la vista compila e importa. Compárala con tu versión capa por capa.
Ver solución · logica.py
python
"""Reglas puras: sin widgets, sin archivos."""
def validar_tarea(texto: str) -> tuple[bool, str]:
if not texto.strip():
return False, "La tarea no puede estar vacía"
return True, ""Ver solución · datos.py (JSON)
python
"""Capa de datos: lo único que sabe CÓMO se guarda."""
import json
from pathlib import Path
RUTA = Path.home() / ".mi_todo" / "tareas.json"
def cargar() -> list[dict]:
try:
with open(RUTA, encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return []
def guardar(tareas: list[dict]) -> None:
RUTA.parent.mkdir(exist_ok=True)
with open(RUTA, "w", encoding="utf-8") as f:
json.dump(tareas, f, ensure_ascii=False, indent=2)Ver solución · app.py (la vista)
python
"""Vista: SOLO widgets y callbacks; delega en logica y datos."""
import customtkinter as ctk
import logica
import datos
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Mi To-Do")
self.geometry("420x520")
self._tareas = datos.cargar() # restaurar al abrir
self._crear_widgets()
self._pintar()
def _crear_widgets(self):
self.entry = ctk.CTkEntry(self, placeholder_text="Nueva tarea…")
self.entry.pack(padx=12, pady=12, fill="x")
ctk.CTkButton(self, text="Agregar", command=self._agregar).pack()
self.lista = ctk.CTkScrollableFrame(self)
self.lista.pack(fill="both", expand=True, padx=12, pady=12)
self.status = ctk.CTkLabel(self, text="")
self.status.pack()
def _agregar(self):
ok, msg = logica.validar_tarea(self.entry.get()) # ← capa lógica
if not ok:
self.status.configure(text=f"⚠️ {msg}")
return
self._tareas.append({"texto": self.entry.get().strip(), "hecha": False})
datos.guardar(self._tareas) # ← capa datos
self.entry.delete(0, "end")
self._pintar()
def _alternar(self, tarea):
tarea["hecha"] = not tarea["hecha"]
datos.guardar(self._tareas)
self._pintar()
def _pintar(self):
for w in self.lista.winfo_children():
w.destroy()
for tarea in self._tareas:
icono = "✅" if tarea["hecha"] else "⬜"
ctk.CTkCheckBox(self.lista, text=f"{icono} {tarea['texto']}",
command=lambda t=tarea: self._alternar(t)
).pack(anchor="w", pady=2)
pendientes = sum(1 for t in self._tareas if not t["hecha"])
self.status.configure(text=f"Pendientes: {pendientes}")Ver solución · main.py
python
from app import App
if __name__ == "__main__":
App().mainloop()❓ Preguntas y respuestas (autoevaluación)
1. ¿En qué capa vive entry.get() y en cuál "el email debe tener @"?
entry.get()en la vista (widget); la regla del email en la lógica.
2. ¿Hacia dónde apuntan los imports entre capas?
Siempre "hacia abajo": ui → logica → datos. Nunca al revés.
3. ¿Qué ganas concretamente con la separación?
Probar lógica/datos sin GUI, cambiar el almacenamiento (JSON→SQLite) sin tocar la vista, y reusar la lógica en otra interfaz (CLI, web).
4. ¿Por qué main.py debe ser mínimo?
Es solo el punto de entrada: si acumula lógica, vuelve a mezclar capas y no se puede importar limpio (rompe el guardián
__main__).
📎 Apuntes relacionados
➡️ Siguiente
Clase 30 · SQLite I — la capa de datos de verdad: crear la BD e insertar.