Nuevo capítulo del libro de Clean Code: Funciones

Uncle Bob nos da 2 consejos de cómo debe ser una función:

  1. Las funciones deben ser cortas
  2. Las funciones deben ser más cortas todavía

Esto, que parece un juego de palabras, ha sido una de las mayores revelaciones para mi. De hecho, cuando lo leí me mostraba escéptico al respecto y no fue hasta que lo puse en práctica que no me volví un psicópata de las funciones cortas.

Pero ¿qué es una función corta?, hasta ahora yo intentaba seguir el siguiente patrón:

  • Si no tienes que hacer scroll para leer toda la función en pantalla, el tamaño es adecuado.

MAL!!

Es muy fácil hacer trampas con eso, dame:

  • Una pantalla 4K
  • Ultrawide
  • Con fuente de tamaño 10pt
  • Pantalla en posición vertical
  • Déjame escribir varias líneas en una (en lenguajes con separador “;” es legal)

Y cumpliré con la norma teniendo funciones de 250 líneas!!!

Bob nos recomienda funciones de no más de 20 líneas y esta deberían ser funciones descriptivas, no algoritmicas.

Quien haya leído el libro dirá “descriptivas? algoritmicas? qué me estás container?"

Sí, me acabo de inventar el concepto de funciones descriptivas y funciones algoritmicas #patentFree

  • Una función descriptiva es aquella que te explica qué hace con “lenguaje natural”, solamente.
  • Una función algoritmica es aquella que hace algo específico haciendo uso de lo que todos conocemos: condiciones, bucles, etc…

Pero basta de cháchara y veámoslo con ejemplos.

Comienza la aventura!

El padre de Bulma, el Dr Brief, está construyendo una nueva sala de entrenamiento para Vegeta.

Cómo sabéis Vegeta es un fan de entrenar bajo una fuerza de gravedad extrema y está preparando la máquina para generar unos 1000G.

Había delegado el desarrollo del firmware de la máquina a becarios del Instituto Orange Star. Pero no pudo darles todo el soporte que hubiera deseado porque tenía otras responsabilidades que atender con su gatito Tama.

Revisando el código, vio que tenía funciones demasiado largas y que para una máquina que te comprimía a base de aumentar la fuerza de gravedad, no era adecuado que tuviera ese tipo de funciones así que se dispuso a comprimirlas, a hacerlas más cortas.

public class GravityManager {
    public void mainProcess() {
        float planetGravity = 0.0;
        float resultingGravity = 0.0;
        int gravityMultiplier = 0;

        PlanetDictionary dict = new PlanetDictionary();
        PlanetGravityReader reader = new PlanetGravityReader();

        Planet planet = dict.getCurrentPlanet();
        planetGravity = reader.readGravityFromPlanet(planet.getId());
        if (planetGravity > 0) {
            NumPadInputter inputter = new NumPadInputter();
            while (inputter.isReading()) {
                gravityMultiplier = inputter.getInput();
            }
            resultingGravity = planetGravity * gravityMultiplier;
            ScreenOutput.showMessage("New gravity value: " + resultingGravity);
        } else {
            ScreenOutput.showMessage("Zero gravity can not be multiplied");
        }

        GravitationalForceAccelerator gaf = new GravitationalForceAccelerator();
        gaf.setAccelerateFrom(planetGravity);
        gaf.setAccelerateTo(resultingGravity);
        gaf.startSlowGravityAcceleration();

        RawSoundWave sound = SoundOutput.DigitalSound.parseString("PI PO PI, PO PI PI PO PIIIII");
        SoundOutput.play(sound);

        ScreenOutput.showMessage("Increasing gravity...");

        float first25percent = (resultingGravity - planetGravity) * 0.25f;
        float halfpercent = first25percent + first25percent;
        float threequarters = first25percent * 3;
        float hundredpercent = resultingGravity;
        float currentGravity = planetGravity;

        while (currentGravity <= first25percent) {
            sound = SoundOutput.DigitalSound.parseString("PI");
            sound.setBassDbTo(sound.getDefaultBassDb() + 30);
            SoundOutput.play(sound);
            Thread.sleep(5000);
            currentGravity = gaf.getCurrentGravity();
        }

        while (currentGravity <= halfpercent) {
            sound = SoundOutput.DigitalSound.parseString("PI");
            sound.setBassDbTo(sound.getDefaultBassDb() + 10);
            SoundOutput.play(sound);
            Thread.sleep(3000);
            currentGravity = gaf.getCurrentGravity();
        }

        while (currentGravity <= threequarters) {
            sound = SoundOutput.DigitalSound.parseString("PI");
            sound.setTrebleDbTo(sound.getDefaultTrebleDb() + 10);
            SoundOutput.play(sound);
            Thread.sleep(2000);
            currentGravity = gaf.getCurrentGravity();
        }

        while (currentGravity <= hundredpercent) {
            sound = SoundOutput.DigitalSound.parseString("PI");
            sound.setTrebleDbTo(sound.getDefaultTrebleDb() + 30);
            SoundOutput.play(sound);
            Thread.sleep(1000);
            currentGravity = gaf.getCurrentGravity();
        }

        sound = SoundOutput.DigitalSound.parseString("PI PI PI");
        sound.setBassDbTo(sound.getDefaultBassDb());
        sound.setTrebleDbTo(sound.getDefaultTrebleDb());
        SoundOutput.play(sound);

        ScreenOutput.showMessage("New gravity increased by: " + gravityMultiplier);

        gaf.close();

        EmergencyListener emergency = new EmergencyListener();
        emergency.setExplosionDetection(true);

        VitalSignalsMonitor vsm = new VitalSignalsMonitor();
        HeartBeatMonitor hbm = vsm.getHeartBeatMonitor();
        hbm.setMinimumTo(40);
        hbm.setMaximumRo(300);
        vsm.startHeartBeatMonitor(hbm);
        vsm.startBreathingMonitor();
        emergency.setVitalSignalsMonitor(vsm);

        Contacts contacts = AdressBook.getContacts();
        Contact bulma = contacts.getContactByName("Bulma");
        emergency.setEmergencyPhoneNumber(bulma.getPhoneNumber());

        emergency.startListen();

        LightsController lights = new LightsController();

        if (gravityMultiplier > 50) {
            lights.setColorTo(Colors.RED);
        }

        TrainingRobotsCollection robots = TrainingRobots.invokeRobotsOfType("Killer Robot").multiplyBy(30);
        robots.build();
        robots.attack();
    }
}

El pobre Dr Brief estuvo un bueeeen rato leyendo y entendiendo la función.

Esa función hacía demasiadas cosas, y es que ese nombre tan genérico: mainProcess no auguraba nada bueno.

El Dr Brief se arremangó, acarició a Tama y se dispuso a mejorar ese código. empezaría por el principio, por una función descriptiva.

private void initGravitySystem() {
    setNewGravity();
    giveFeedbackWhileSettingGravity();
    startEmergencyService();
    setRoomAmbient();
    startTrainingSet();
}

El sistema debía hacer 5 cosas para configurar una sesión de entrenamiento:

  1. Activar la nueva configuración de fuerza de gravedad.
  2. Dar información del estado este proceso, que no es inmediato.
  3. Activar el sistema de emergencias.
  4. Configurar el ambiente de la habitación de entrenamiento
  5. Iniciar la configuración de entrenamiento deseada

En esta primera función del Dr Brief vemos el guión de lo que iniciar el sistema de gravedad va a hacer.

Es una función descriptiva, en tan sólo 5 líneas nos explica que va a pasar y si queremos más nivel de detalle, acederemos a cada función.

Siguió desglosando las funciones…

private void setNewGravity() {
    getCurrentPlanetGravity(),
    getTargetGravity();
    startGravityAcceleration();
}

Una nueva función descriptiva, 3 líneas, que nos sirve de guión.

De momento el Dr. Brief no está entrando en los detalles de qué parámetros van y vienen, ya lo ajustará según correspondan.

La siguiente función a revisar giveFeedbackWhileSettingGravity();… no… mejor no, vamos acabar un flujo de ejecución y luego vamos a por otro, el proceso es secuencial, ¿por qué nos vamos a complicar en definir primero las de más alto nivel, luego otro nivel y así sucesivamente? Sólo tendremos un montón de código incompleto hasta que lo acabemos todo…

Seguimos definiendo lo que hay en la neuva setNewGravity();

public float getCurrentPlanetGravity() {
    PlanetGravityReader gravityReader = new PlanetGravityReader();
    PlanetDictionary allPlanets = new PlanetDictionary();
    Planet currentPlanet = allPlanets.getCurrentPlanet();
    int planetId = currentPlanet.getId();
    float planetGravity = gravityReader.readGravityFromPlanet(planetId);
    return planetGravity;
}

Esta ya es una función algorítmica, 6 líneas, actúa directamente con clases y funciones y hace uso de los valores retornados como parámetros de otras funciones, una función “nada especial”. Seguro?

El Dr Brief, gracias al contenido tamaño de las funciones y su contexto hiper-concreto, se ha permitido ya hacer algo de refactor para hacer más legible la función, que aún no siendo “descriptiva pura”, leerla, casi casi, es como leer el guión de lo que hace.

public void getTargetGravity(float planetGravity) {
    int gravityMultiplier = 0;
    NumPadInputter inputter = new NumPadInputter();
    while (inputter.isReading()) {
        gravityMultiplier = inputter.getInput();
    }
    return planetGravity * gravityMultiplier;
}

Otra función, 6 líneas esta vez. El Dr Brief ha extraído lo importante de esta función, el algoritmo inicial mezclaba muchas cosas, aquí sólo debemos obtener la gravedad objetivo, la que nos entran por el NumPad de la máquina. El Enviar mensajes al outuput lo gestionaría a parte ya veremos dónde. Siguiente función:

public void startGravityAcceleration(fromGravity, toGravity) {
    GravitationalForceAccelerator gaf = new GravitationalForceAccelerator();
    gaf.setAccelerateFrom(fromGravity);
    gaf.setAccelerateTo(toGravity);
    gaf.startSlowGravityAcceleration();
}

Esta era fácil, 4 líneas, además se ha permitido renombrar las variables de manera que la firma de la función es genérica y entendible para reusarla en otros lugares.

Esa es otra, las funciones van montando la API de tu programa, si haces muchas cosas dentro de una función larga, esa función larga sólo servirá para satisfacer un caso de uso concreto. Si tienes muchas micro-funciones troceadas, podrás montar nuevos algoritmos haciendo mix de las funciones que tengas.

El Dr. Brief debería empezar a preocuparse de mostrar algo por el Output, pero se ha dado cuenta de una cosa.

Al separar las funciones, y contextualizarlas mejor ha detectado un bug!

Resulta que el algoritmo original avisaba en pantalla de que si estabas en gravedad cero no podías multiplicar la gravedad:

    ...
    if (planetGravity > 0) {
        ...
        ScreenOutput.showMessage("New gravity value: " + resultingGravity);
    } else {
        ScreenOutput.showMessage("Zero gravity can not be multiplied");
    }

    GravitationalForceAccelerator gaf = new GravitationalForceAccelerator();
    ...

Pero después, no había más control y arrancaba el sistema gravitacional, el de emergencia y activaba el entrenamiento. No tenía sentido!

Era un buen momento para arreglar aquello, ahora que ya tenía definidas las funciones de setNewGravity.

public boolean startGravityAcceleration(fromGravity, toGravity) {
    if (toGravity <= 0)
        return false;
    GravitationalForceAccelerator gaf = new GravitationalForceAccelerator();
    gaf.setAccelerateFrom(fromGravity);
    gaf.setAccelerateTo(toGravity);
    gaf.startSlowGravityAcceleration();
    return true;
}

Ajustó startGravityAcceleration, 7 líneas, para que no arrancara el acelerador si íbamos a acelerar a gravedad cero. El sistema para desacelerar hace uso de otra API no la de aceleración así que sólo tenía que controlar la gravedad target.

private boolean setNewGravity() {
    float planetGravity = getCurrentPlanetGravity(),
    float targetGravity = getTargetGravity(planetGravity);
    boolean isNewGravitySet = startGravityAcceleration(planetGravity, toGravity);
    return isNewGravitySet;
}

Finalmente, repasó los parámetros de las funciones de setNewGravity y ajustó su retorno para controlar los mensajes a posteriori, 4 líneas.

Ya tenemos la primera función a gusto del Dr. Brief, vuelve a ajustar lo definido al principio:

private void initGravitySystem() {
    boolean isNewGravitySet = setNewGravity();
    giveFeedbackWhileSettingGravity();
    startEmergencyService();
    setRoomAmbient();
    startTrainingSet();
}

Y… aventura pausada!

Aún le queda mucho trabajo al Dr Brief, pero no vamos a desvelarlo todo en un único post, veamos cómo lo lleva hasta el momento a modo de resumen y continuamos en el próximo post!

public class GravityManager {
    // 5 líneas
    private void initGravitySystem() {
        boolean isNewGravitySet = setNewGravity();
        giveFeedbackWhileSettingGravity();
        startEmergencyService();
        setRoomAmbient();
        startTrainingSet();
    }
    // 4 líneas
    private boolean setNewGravity() {
        float planetGravity = getCurrentPlanetGravity(),
        float targetGravity = getTargetGravity(planetGravity);
        boolean isNewGravitySet = startGravityAcceleration(planetGravity, toGravity);
        return isNewGravitySet;
    }
    // 6 líneas
    public float getCurrentPlanetGravity() {
        PlanetGravityReader gravityReader = new PlanetGravityReader();
        PlanetDictionary allPlanets = new PlanetDictionary();
        Planet currentPlanet = allPlanets.getCurrentPlanet();
        int planetId = currentPlanet.getId();
        float planetGravity = gravityReader.readGravityFromPlanet(planetId);
        return planetGravity;
    }
    // 6 líneas
    public void getTargetGravity(float planetGravity) {
        int gravityMultiplier = 0;
        NumPadInputter inputter = new NumPadInputter();
        while (inputter.isReading()) {
            gravityMultiplier = inputter.getInput();
        }
        return planetGravity * gravityMultiplier;
    }
    // 7 líneas
    public boolean startGravityAcceleration(fromGravity, toGravity) {
        if (toGravity <= 0)
            return false;
        GravitationalForceAccelerator gaf = new GravitationalForceAccelerator();
        gaf.setAccelerateFrom(fromGravity);
        gaf.setAccelerateTo(toGravity);
        gaf.startSlowGravityAcceleration();
        return true;
    }
}

Agradecimientos

Gracias a mis compañeros y compañeras de Basetis por el acceso a este libro y la flexibilidad para escribir este contenido que comparto con vosotras.