Capítol 10

Codi net

Que un programa funcioni no vol dir que estigui ben escrit. En aquest capítol veuràs per què val la pena escriure codi net, amb exemples concrets i situacions que reconeixeràs.

El problema del copia-enganxa

En Karel ha de recórrer un corredor recollint totes les perles que troba. Aquí tens dues solucions que fan exactament el mateix. Llegeix-les i fixa't en les diferències:

✗ Codi brut
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
✓ Codi net
def avançar_i_recollir():
    move()
    if pearl_here():
        grab()

while front_is_clear():
    avançar_i_recollir()

El codi brut té un problema molt concret: si el corredor canvia de mida, has d'afegir o treure línies a mà. Si vols canviar la lògica —per exemple, deixar una perla en lloc de recollir-la— has de fer el canvi en sis llocs alhora. I és molt fàcil oblidar-ne un.

La versió neta diu literalment el que fa: «mentre puguis avançar, avança i recull». Funciona per a qualsevol mida de corredor sense tocar res. Comprova-ho: el codi és idèntic en els tres mons però s'adapta als corredors de 5, 7 i 10 caselles:

Aquesta és la primera regla del codi net: no dupliquis codi innecessàriament. Quan veus el mateix bloc repetit dues o tres vegades, és el moment de crear una funció i usar un bucle.

Aquesta idea té un nom en el món de la programació: DRY, que significa Don't Repeat Yourself («no et repeteixis»). El seu contrari —el codi que acabes de veure repetit sis vegades— es diu WET (Write Everything Twice). Quan escrius codi net, escrius codi DRY.

Els noms importen

El nom que li poses a una funció és la primera cosa que llegirà qualsevol persona que vegi el teu codi, inclòs tu mateix l'endemà. Aquí tens dues versions d'un programa que fan el mateix: en Karel recorre un corredor i recull cada perla que troba:

✗ Noms que no expliquen res
def p():
    if pearl_here():
        grab()

while front_is_clear():
    move()
    p()
✓ Noms descriptius
def recollir_si_hi_ha_perla():
    if pearl_here():
        grab()

while front_is_clear():
    move()
    recollir_si_hi_ha_perla()

Amb p() no pots saber què fa la funció sense llegir el seu interior. Amb recollir_si_hi_ha_perla(), el codi principal es llegeix com una frase: «mentre puguis avançar, avança i recull si hi ha perla». No cal ni anar a mirar la definició.

Observa-ho en acció. El codi de la dreta —la versió neta— és el que s'executa:

La segona regla: posa noms que expliquen el que fa la funció. recollir_si_hi_ha_perla() és molt millor que p(). avançar_fins_a_paret() és molt millor que f(). El nom és la documentació més bàsica que pots donar al teu codi.

Una funció, una idea

Cada funció hauria de fer una sola cosa i tenir un nom que la descrigui exactament. Si quan expliques el que fa una funció necessites dir «fa X i també fa Y», és senyal que potser hauries de partir-la en dues.

Aquí en Karel ha d'avançar fins al final d'un corredor i deixar-hi una perla:

✗ Una funció fa dues coses
def anar_i_deixar_perla():
    while front_is_clear():
        move()
    drop()

anar_i_deixar_perla()
✓ Una funció, una responsabilitat
def avançar_fins_a_paret():
    while front_is_clear():
        move()

# Cada acció és independent i reutilitzable.
avançar_fins_a_paret()
drop()

La versió neta té un avantatge clar: avançar_fins_a_paret() és tan genèrica que la pots reutilitzar en qualsevol repte futur, en qualsevol direcció, sense importar el que vulguis fer en arribar. La versió bruta lliga el moviment amb l'acció de deixar una perla, de manera que la funció només serveix per a aquest cas concret i no pot aprofitar-se en cap altre context.

La tercera regla: cada funció, una idea. Funcions petites i enfocades en una sola tasca són fàcils d'entendre, fàcils de modificar i molt fàcils de reutilitzar en altres programes.

Codi net avui, temps guanyat demà

El codi net no és qüestió d'estètica. Té efectes molt concrets i pràctics que notes des del primer dia:

⏱ A curt termini

  • Trobes errors més de pressa. Amb avançar_i_recollir() saps exactament on mirar. Amb f() i bb() has de llegir funció per funció fins a entendre qui fa què.
  • Modifiques en un sol lloc. Vols que en Karel deixi dues perles en lloc d'una? Canvies la definició de la funció i el canvi s'aplica arreu. Amb codi repetit, has de buscar i modificar cada còpia.
  • Rellegeixes el teu codi l'endemà. while front_is_clear(): avançar_i_recollir() s'entén d'un cop d'ull. Sis blocs iguals copiats t'obliguen a reubicar-te de zero.

📅 A mig termini

  • Reutilitzes el que ja has escrit. Tens recollir_si_hi_ha_perla() d'un repte anterior? La copies directament al repte nou. Funciona sense errors, sense reescriure res.
  • Construeixes comportaments complexos. avançar_fins_a_paret() + recollir_si_hi_ha_perla() + turn_around() combinades donen un programa sofisticat llegible en tres línies.
  • El codi s'adapta sol. Si el món canvia —corredor més llarg, més perles— el teu codi net sovint no cal tocar-lo. El brut, sí.

🌍 A llarg termini

  • Pots compartir-lo. Un company llegeix while front_is_clear(): avançar_i_recollir() i l'entén sense que li expliquis res. El codi brut necessita una explicació verbal.
  • El codi es llegeix molt més que no s'escriu. En el món professional, el mateix codi el llegeixen desenes de persones durant anys. Escriure'l net és un acte de respecte cap als qui vindran després (incloent el tu del futur).
  • Col·laborar es torna possible. Sense noms clars i funcions petites, treballar en equip en un mateix projecte és un caos.

En resum, tres regles simples:

  • No dupliquis codi innecessàriament. Si veus el mateix bloc repetit dues o tres vegades, crea una funció i usa un bucle.
  • Posa noms que expliquen. avançar_i_recollir() sempre és millor que fer_cosa().
  • Cada funció, una idea. Si quan expliques una funció uses la paraula «i també», considera partir-la en dues.
EXERCICI 1 — Fàcil

Elimina la repetició

El codi de sota funciona, però és brut: copia el mateix bloc set vegades. La teva tasca és reescriure'l de manera neta usant una funció i un while. El resultat ha de ser el mateix: en Karel ha de recórrer tot el corredor i recollir totes les perles.

Codi brut que has de netejar:

# ✗ NO copiïs això — reescriu-lo net!
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()
move()
if pearl_here():
    grab()

Pista: crea una funció avançar_i_recollir() que faci el move() i el if pearl_here(): grab(). Després usa un while front_is_clear(): per cridar-la tantes vegades com calgui, sigui quin sigui el corredor.

EXERCICI 2 — Mig

Noms i estructura

En Karel ha de recórrer el corredor horitzontal, girar a la dreta i recollir la perla de la cantonada. El codi de sota ho fa, però té dos problemes: els noms de les funcions no expliquen res, i els moviments estan escrits un per un en lloc d'usar un while. Reescriu-lo net.

Codi brut que has de netejar:

# ✗ NO copiïs això — reescriu-lo net!
# Problema 1: f i g no diuen res sobre el que fan
# Problema 2: els tres move() s'haurien d'expressar amb while
def f():
    turn_left()
    turn_left()
    turn_left()

def g():
    if pearl_here():
        grab()

move()
move()
move()
f()
move()
g()

Pista: canvia els noms de f i g per noms que descriguin el que fan. Afegeix una funció avançar_fins_a_paret() que usi while front_is_clear(): move() en lloc dels tres move() explícits. Recorda que turn_right() existeix com a instrucció directa si vols simplificar el gir.

EXERCICI 3 — Difícil

Neteja total

En Karel ha de recórrer el corredor d'esquerra a dreta recollint les perles, girar i tornar deixant-les una a una. El codi de sota ho fa correctament, però és un desastre: sis funcions sense nom, dos bucles escrits com a repeticions explícites, i un gir amb dos turn_left() que es podria fer en una sola instrucció. Troba tots els problemes i reescriu el codi completament net.

Codi brut que has de netejar:

# ✗ NO copiïs això — reescriu-lo net!
def aa():
    move()

def bb():
    if pearl_here():
        grab()

def cc():
    aa()
    bb()

def dd():
    turn_left()
    turn_left()

def ee():
    if not bag_is_empty():
        drop()

def ff():
    aa()
    ee()

cc()
cc()
cc()
cc()
cc()
cc()
cc()
dd()
ff()
ff()
ff()
ff()
ff()
ff()
ff()

Set problemes a corregir:

  • Les funcions aa, bb, cc, dd, ee, ff no expliquen res amb els seus noms.
  • cc() fa dues coses alhora: avançar i recollir. Viola la regla «una funció, una idea».
  • ff() fa dues coses alhora: avançar i deixar una perla. Mateixa violació.
  • dd() usa dos turn_left() per fer mitja volta, quan existeix la instrucció turn_around().
  • Els set cc() consecutius s'han de substituir per un while front_is_clear():.
  • Els set ff() consecutius s'han de substituir per un while front_is_clear():.
  • Les funcions aa i bb gairebé no calen si defineixes bé les funcions compostes.