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

  1. Del requisit a la tasca: les sis fites
  2. L'ordre de construcció i els seus perquès
  3. Arquitectura en capes: la web tradueix, el paquet resol
  4. Estructura de directoris del projecte
  5. Disseny de dades: els tres fitxers
  6. Contractes abans que codi
  7. Decisions de disseny i els seus perquès
  8. 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.py importa 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: RepositoriCataleg amb 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.md qualsevol 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 és int, float o Llibre.

Exercicis

  1. Fita 0.4 — Esquelet del projecte. Crea l'arbre de directoris complet (amb __init__.py, fitxers buits o amb pass, 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, i pytest s'executa (encara que sigui amb "no tests ran").
  2. Fita 0.5 — Contractes complets. Copia la taula de contractes a DECISIONS.md i 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.
  3. 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

  1. Criteri de verificació addicional: des de l'arrel, python -c "from papyrus import models, errors, serveis, repositoris" no ha de fallar. Si falla amb ModuleNotFoundError, revisa que papyrus/__init__.py existeix i que executes des de l'arrel (el clàssic de M3).
  2. Exemple de contracte de ruta (itinerari A): POST /api/vendes — entrada JSON {"titol": str, "unitats": int, "soci": str|null, "cupo": str|null} — respostes 201 amb la venda creada, 404 si el títol no existeix, 409 si no hi ha estoc, 400 si 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.
  3. 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats