Tens el contracte (12-01); ara toca el plànol. Els projectes personals no solen fracassar per falta de coneixements, sinó per falta d'estructura: es comença pel que llueix (la web), es descobreix a mitges que el nucli no aguanta, i el refactor desmoralitza. Aquesta lliçó converteix els requisits en un pla executable: sis fites ordenades amb criteri, una arquitectura en capes, l'estructura de directoris definitiva, els esquemes de dades i — la part més valuosa — els contractes de les funcions clau escrits abans de programar-les. En acabar tindràs una cosa que molt poca gent té en començar un projecte: claredat sobre què fer cada dia i com saber que està fet.
Contingut
- Del requisit a la tasca: les sis fites
- L'ordre de construcció i els seus perquès
- Arquitectura en capes: la web tradueix, el paquet resol
- Estructura de directoris del projecte
- Disseny de dades: els tres fitxers
- Contractes abans que codi
- Decisions de disseny i els seus perquès
- Gestió del temps per a autodidactes
Del requisit a la tasca: les sis fites
Un requisit ("gestió de catàleg") no es pot començar un dimarts a la tarda; una tasca ("implementar RepositoriCataleg.carregar()") sí. El pont entre l'un i l'altra són les fites: paquets de tasques que acaben en alguna cosa demostrable. Papyrus Online es construeix en sis:
| Fita | Què lliura | Requisits que cobreix | Esforç relatiu | "Demo" en acabar-la |
|---|---|---|---|---|
| H1 — Domini | models.py, errors.py, cupons.py nets i anotats |
RF1 (model), RF2, RF3 (regles), RNF2, RNF3 | ★★ | En un REPL: crear llibres, calcular 12.35 € |
| H2 — Serveis | serveis.py amb ServeiVendes integrat |
RF2, RF3, RF7 | ★★★ | Vendre amb soci+cupó des d'un script |
| H3 — Persistència | repositoris.py (JSON/CSV, guardat atòmic) |
RF4 | ★★ | Vendre, reiniciar, l'estoc persisteix |
| H4 — Interfície | API Flask o web Django mínima | RF5 | ★★★ (A) / ★★★★ (B) | Vendre des del navegador/curl |
| H5 — Informe | informe.py amb números + PNG |
RF6 | ★★ | El PNG del mes a dades/ |
| H6 — Poliment | Logging complet, README, requirements | RF7, RNF4 | ★ | Una altra persona l'instal·la i l'usa |
Els tests no són una fita: acompanyen cada fita (H1 es tanca amb els tests d'H1 en verd). Deixar els tests "per al final" és la recepta del final que no arriba mai; ho vas veure a M9 i aquí s'aplica.
Les estrelles són esforç relatiu, no hores: si H1 et costa una sessió, espera'n unes tres per a H2. Calibra amb la teva pròpia velocitat després de la primera fita — és la dada més honesta que tindràs.
L'ordre de construcció i els seus perquès
L'ordre no és negociable capriciosament: segueix les dependències. No pots provar l'API sense serveis, ni els serveis sense domini. L'informe només necessita el CSV, així que va després de la persistència.
flowchart TD
H1["H1 Domini<br/>Llibre, Soci, errors, cupons"] --> H2["H2 Serveis<br/>ServeiVendes"]
H2 --> H3["H3 Persistència<br/>repositoris JSON/CSV"]
H3 --> H4["H4 Interfície<br/>Flask o Django"]
H3 --> H5["H5 Informe<br/>pandas + Matplotlib"]
H4 --> H6["H6 Poliment<br/>logging, README"]
H5 --> H6
Per què core → persistència → interfície → dades i no a l'inrevés?
- El domini primer perquè tota la resta el consumeix i perquè és el més barat de provar: funcions pures, sense fitxers ni HTTP. Un error d'arrodoniment detectat a H1 costa un minut; detectat a H4, una tarda de perseguir-lo a través de tres capes.
- La persistència abans que la interfície perquè la interfície necessita dades reals per servir, i perquè el guardat atòmic condiciona el disseny del servei (qui guarda, el servei o l'endpoint? — ho decidim més avall).
- La interfície després perquè és la capa que tradueix, no la que resol: quan hi arribis, cada endpoint serà una funció de 10 línies que crida un servei ja provat. Així es nota que el disseny és bo: la part "difícil" resulta fàcil.
- L'informe gairebé al final perquè només depèn del CSV: és independent de la interfície (fixa-t'hi: H4 i H5 no tenen fletxa entre si — si un dia t'encalles a la interfície, avança l'informe; tenir feina paral·lelitzable és or per a la moral).
Arquitectura en capes
La regla de M10 — "la web tradueix, el paquet resol" — es converteix aquí en arquitectura explícita de quatre capes:
flowchart TD
subgraph Interficie["Capa d'interfície (app.py / views.py)"]
direction LR
A["HTTP → crides al servei<br/>errors de domini → codis HTTP"]
end
subgraph Serveis["Capa de serveis (serveis.py)"]
B["ServeiVendes: orquestra regles,<br/>repositoris i logging"]
end
subgraph Domini["Capa de domini (models, errors, cupons)"]
C["Regles pures: preus, descomptes,<br/>validacions. Sense fitxers, sense HTTP"]
end
subgraph Persistencia["Capa de persistència (repositoris.py)"]
D["Carregar/guardar JSON i CSV.<br/>Sense regles de negoci"]
end
Interficie --> Serveis
Serveis --> Domini
Serveis --> Persistencia
Dues regles de dependència que ho sostenen tot:
- Cap avall, mai cap amunt: el domini no sap que Flask existeix; els repositoris no saben què és un descompte. Si
models.pyimporta alguna cosa d'app.py, el disseny s'ha trencat (i probablement tens un import circular, vella trampa de M3). - La capa de serveis és l'única que orquestra: domini (calcular preu) + persistència (guardar) + logging. Els endpoints no toquen mai un fitxer ni calculen un preu; només tradueixen HTTP ↔ servei.
El premi pràctic: els tests. El domini es prova sense disc ni xarxa (ràpid); els repositoris amb tmp_path (M9); la interfície amb el test client. Cada capa, amb l'eina que li toca — ho veuràs a 12-04.
Estructura de directoris del projecte
Aquest és l'arbre de l'itinerari A (el B substitueix app.py pel projecte Django de 10-04, i l'ORM assumeix cataleg.json):
papyrus_online/ ├── papyrus/ # el paquet: TOTA la logica viu aqui │ ├── __init__.py │ ├── models.py # Llibre, Soci (dataclasses, M5) │ ├── errors.py # jerarquia ErrorPapyrus (M7) │ ├── cupons.py # CUPONS, aplicar_cupo (M7) │ ├── serveis.py # ServeiVendes (nou, H2) │ ├── repositoris.py # RepositoriCataleg, RepositoriSocis, RegistreVendes (H3) │ └── informe.py # informe mensual amb pandas (H5) ├── app.py # interficie Flask: nomes tradueix (H4, itinerari A) ├── dades/ │ ├── cataleg.json # els 4 llibres canonics │ ├── socis.json # LLUIS-001, MARTA-002, PAU-003 │ ├── vendes.csv # es crea en vendre │ └── papyrus.log # RF7 ├── tests/ │ ├── conftest.py # fixtures canoniques (els 4 llibres) │ ├── test_models.py │ ├── test_serveis.py │ ├── test_repositoris.py │ └── test_app.py # test client ├── DECISIONS.md # el teu diari de decisions (fita 0.1) ├── README.md # RNF4 (plantilla a 12-05) └── requirements.txt
Observa que magatzem.py desapareix: les seves responsabilitats es reparteixen entre serveis.py (les regles: vendre, reposar, tancament de caixa) i repositoris.py (carregar/guardar). És l'evolució natural del disseny de M5-M6 ara que saps separar capes.
Disseny de dades: els tres fitxers
Fixar els esquemes abans de programar evita la pitjor classe de bug: dos mòduls que entenen el mateix fitxer de maneres diferents.
dades/cataleg.json — llista d'objectes, títol únic:
[
{"titol": "L'Odissea", "autor": "Homer", "preu": 12.50, "estoc": 4},
{"titol": "Hamlet", "autor": "Shakespeare", "preu": 9.95, "estoc": 6},
{"titol": "El Quixot", "autor": "Cervantes", "preu": 15.90, "estoc": 8},
{"titol": "Faust", "autor": "Goethe", "preu": 21.00, "estoc": 10}
]dades/socis.json — codi únic com a clau natural:
[
{"codi": "LLUIS-001", "nom": "Lluis", "alta": "2025-03-12"},
{"codi": "MARTA-002", "nom": "Marta", "alta": "2025-06-30"},
{"codi": "PAU-003", "nom": "Pau", "alta": "2026-01-15"}
]dades/vendes.csv — una fila per venda, amb capçalera:
| Columna | Tipus | Exemple | Nota |
|---|---|---|---|
data |
YYYY-MM-DD |
2026-07-13 |
format ISO, com el date de tot el curs |
titol |
str | Faust |
ha d'existir al catàleg en el moment de la venda |
unitats |
int ≥ 1 | 2 |
|
quantia |
float, 2 decimals | 37.34 |
quantia total de la línia, amb els descomptes ja aplicats |
Tipus clars = menys sorpreses: preu i estoc són float i int en JSON, no strings; unitats al CSV arribarà com a text i algú (el repositori, no l'informe) l'ha de convertir. Decidir-ho ara, aquí, és disseny.
Contractes abans que codi
Els type hints de 08-01 no eren un ornament: són especificació. Escriure les signatures abans que els cossos t'obliga a decidir entrades, sortides i errors quan encara és barat canviar d'opinió. Aquests són els contractes del cor del sistema:
| Funció / mètode | Signatura (contracte) | Errors que llança |
|---|---|---|
Llibre.preu_final |
(self, soci: bool = False) -> float |
— |
aplicar_cupo |
(preu: float, codi: str) -> float |
CupoInvalidError |
ServeiVendes.vendre |
(self, titol: str, unitats: int, codi_soci: str | None = None, cupo: str | None = None) -> Venda |
LlibreNoTrobatError, EstocInsuficientError, SociInvalidError, CupoInvalidError |
ServeiVendes.tancament_de_caixa |
(self, data: str) -> float |
— |
RepositoriCataleg.carregar |
(self) -> dict[str, Llibre] |
FileNotFoundError (fitxer absent és error tècnic, no de negoci) |
RepositoriCataleg.guardar |
(self, cataleg: dict[str, Llibre]) -> None |
— (atòmic: o tot o res) |
RegistreVendes.anotar |
(self, venda: Venda) -> None |
— |
generar_informe |
(ruta_csv: Path, mes: str, sortida_png: Path) -> ResumMes |
FileNotFoundError |
Venda i ResumMes seran dataclasses (M5): una venda amb data, titol, unitats, quantia; un resum amb unitats_totals, quantia_total, top_titols, millor_dia. Definir aquests tipus de retorn ja és la meitat del disseny d'H2 i H5.
Fixa't en el que la taula decideix sense escriure ni una línia de codi: que vendre rep el codi de soci (no un booleà — el servei valida contra socis.json, cosa que el preu_final(soci=True) de M5 no podia fer), i que retorna una Venda (no None), perquè la interfície voldrà mostrar la quantia.
Decisions de disseny i els seus perquès
Tota decisió descarta una alternativa. Documentar el perquè (a DECISIONS.md) és el que diferencia un criteri d'una casualitat:
| Decisió | Alternativa descartada | Criteri |
|---|---|---|
Catàleg com a dict[str, Llibre] (clau = títol) |
Llista de llibres | Cerca O(1) i unicitat gratis (M4); la llista obliga a recórrer i a vigilar duplicats |
| El servei guarda després de cada venda | Guardar només en sortir | Si el programa mor, no es perden vendes; el cost (escriure un JSON petit) és menyspreable |
| El cupó s'aplica després del descompte de soci | Abans, o excloents | És la regla de negoci de M7 i la que reprodueixen les quanties canòniques (18.67 €) |
| Errors de negoci = excepcions pròpies | Retornar None / codis |
La interfície distingeix què ha fallat i tria l'HTTP correcte (404 vs 409); None no diu res |
| Vendes en CSV (no JSON) | JSON de vendes | És append-only (una línia per venda, sense reescriure el fitxer) i pandas el llegeix directe per a RF6 |
| Dates com a text ISO | Objectes datetime als fitxers |
JSON no té tipus data; ISO ordena bé com a text i pandas el parseja amb parse_dates |
Quan dubtis entre dues opcions i totes dues semblin vàlides: tria la que sigui més fàcil de provar. És un desempat que gairebé mai no falla.
Gestió del temps per a autodidactes
Sense dates de lliurament ni cap, el risc no és fer-ho malament: és no acabar-ho. Tres pràctiques que funcionen:
- Timeboxing honest: sessions de 60-90 minuts amb un objectiu escrit abans de començar ("avui:
RepositoriCatalegamb els seus tests"). Si l'objectiu no cap a la sessió, era massa gros: parteix-lo. - Definició de "fet" — una fita està feta quan, i només quan: (1) la seva demo de la taula de fites funciona, (2) els seus tests passen, (3) les funcions públiques tenen type hints, i (4) has apuntat a
DECISIONS.mdqualsevol decisió presa. Sense els quatre, està "gairebé feta", que és l'estat on els projectes van a morir. - La regla de la sessió següent: acaba cada sessió anotant quin és el primer pas de la propera. Reprendre un projecte és el que costa més; regala't l'arrencada.
Un ritme orientatiu: H1 en 1-2 sessions, H2 en 2-3, H3 en 2, H4 en 2-3 (A) o 3-4 (B), H5 en 1-2, H6 en 1. Entre 9 i 15 sessions: un projecte de dues a quatre setmanes a ritme d'autodidacta. És un pla, no una promesa — ajusta'l després d'H1 amb la teva velocitat real.
Errors Comuns i Consells
- Dissenyar de més. No necessites interfícies abstractes ni patrons que no entenguis "per si de cas". Quatre capes, vuit contractes i tres esquemes: això és disseny suficient per a aquesta mida. El sobredisseny és procrastinació amb bona consciència.
- Saltar-se els contractes "perquè ja ho tinc al cap". Al cap hi caben dues signatures; a la tercera comencen a contradir-se. Escriu-les: la taula de contractes és la pàgina que més consultaràs a 12-03.
- Confondre capa amb carpeta. Pots tenir les quatre capes en quatre fitxers plans (com el nostre arbre); el que defineix la capa és què importa a què, no on és el fitxer.
- Estimar en hores absolutes. "Això són dues hores" falla sempre; "això és com H1, que em va costar una sessió" falla molt menys. Estima comparant, no endevinant.
- No decidir qui converteix els tipus. El clàssic: el CSV entrega
"2"i l'informe suma strings. La regla d'aquesta lliçó: la capa de persistència entrega tipus correctes; a partir del repositori, tot ésint,floatoLlibre.
Exercicis
- Fita 0.4 — Esquelet del projecte. Crea l'arbre de directoris complet (amb
__init__.py, fitxers buits o ambpass, i els tres fitxers de dades amb el contingut canònic d'aquesta lliçó). Verificació:python -c "import papyrus"funciona des de l'arrel del projecte, ipytests'executa (encara que sigui amb "no tests ran"). - Fita 0.5 — Contractes complets. Copia la taula de contractes a
DECISIONS.mdi afegeix-hi els que falten per al teu itinerari: A) les 6 rutes de l'API amb mètode HTTP, entrada i codis de resposta; B) les vistes i forms. No implementis res: només signatures i errors. - Fita 0.6 — El teu pla. Escriu el teu calendari de fites (dates o nombre de sessions per fita) i la teva definició de "fet" personalitzada. Compromís realista: si només tens 3 sessions per setmana, que el pla ho digui.
Solucions
- Criteri de verificació addicional: des de l'arrel,
python -c "from papyrus import models, errors, serveis, repositoris"no ha de fallar. Si falla ambModuleNotFoundError, revisa quepapyrus/__init__.pyexisteix i que executes des de l'arrel (el clàssic de M3). - Exemple de contracte de ruta (itinerari A):
POST /api/vendes— entrada JSON{"titol": str, "unitats": int, "soci": str|null, "cupo": str|null}— respostes201amb la venda creada,404si el títol no existeix,409si no hi ha estoc,400si el soci o el cupó són invàlids o el JSON està mal format. Si la teva taula no diu quin codi retorna cada error de domini, encara no està acabada. - No hi ha solució única; hi ha una comprovació: ensenya-li el pla al teu jo escèptic. Si una setmana té "H2 + H3 + H4", el teu jo escèptic té raó.
Conclusió
Ja no tens un desig ("fer Papyrus Online"): tens un pla. Sis fites amb demo verificable, un ordre que segueix les dependències reals (domini → serveis → persistència → interfície i informe), una arquitectura de quatre capes on la web tradueix i el paquet resol, esquemes de dades que eliminen ambigüitats i contractes amb type hints que són especificació, no ornament. I una cosa igual d'important: una definició de "fet" i un calendari honest. La propera lliçó és la que fa dotze mòduls que esperes: obrir l'editor i construir, fita a fita, amb esquelets on has de pensar tu i solució completa on la integració ho justifica. El plànol és sobre la taula; ara, els maons.
Curs de Programació en Python
Mòdul 1: Introducció a Python
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
