Problemas para comprender cómo se ve el código limpio en la vida real

10

Actualmente estoy leyendo y trabajando a través de "Clean Code: A Handbook of Agile Software Craftsmanship" de Robert C. Martin. El autor habla sobre cómo una función debe hacer una sola cosa y, por lo tanto, ser relativamente corta. Específicamente Martin escribe:

Esto implica que los bloques dentro de las declaraciones if, las declaraciones else, las declaraciones while, etc. deben tener una línea de longitud. Probablemente esa línea debería ser una llamada de función. Esto no solo mantiene la función de cierre pequeña, sino que también agrega valor documental porque la función llamada dentro del bloque puede tener un nombre muy descriptivo.

Esto también implica que las funciones no deberían ser lo suficientemente grandes como para contener estructuras anidadas. Por lo tanto, el nivel de sangría de una función no debe ser mayor que uno o dos. Esto, por supuesto, hace que las funciones sean más fáciles de leer y comprender.

Esto tiene sentido, pero parece estar en conflicto con ejemplos de lo que veo como código limpio. Tome el siguiente método, por ejemplo:

    public static boolean millerRabinPrimeTest(final int n) {
        final int nMinus1 = n - 1;
        final int s = Integer.numberOfTrailingZeros(nMinus1);
        final int r = nMinus1 >> s;
        //r must be odd, it is not checked here
        int t = 1;
        if (n >= 2047) {
            t = 2;
        }
        if (n >= 1373653) {
            t = 3;
        }
        if (n >= 25326001) {
            t = 4;
        } // works up to 3.2 billion, int range stops at 2.7 so we are safe :-)
        BigInteger br = BigInteger.valueOf(r);
        BigInteger bn = BigInteger.valueOf(n);

        for (int i = 0; i < t; i++) {
            BigInteger a = BigInteger.valueOf(SmallPrimes.PRIMES[i]);
            BigInteger bPow = a.modPow(br, bn);
            int y = bPow.intValue();
            if ((1 != y) && (y != nMinus1)) {
                int j = 1;
                while ((j <= s - 1) && (nMinus1 != y)) {
                    long square = ((long) y) * y;
                    y = (int) (square % n);
                    if (1 == y) {
                        return false;
                    } // definitely composite
                    j++;
                }
                if (nMinus1 != y) {
                    return false;
                } // definitely composite
            }
        }
        return true; // definitely prime
    }
}

Este código está tomado del repositorio de código fuente de Apache Commons en: https://github.com/apache/commons-math/blob/master/src/main/java/org/apache/commons/math4/primes/SmallPrimes.java

El método me parece muy legible. Para implementaciones de algoritmos como este (implementación de la Prueba de Primalidad Probabilística de Miller-Rabin), ¿es adecuado mantener el código como está y aún considerarlo 'limpio', como se define en el libro? ¿O incluso algo ya tan legible como este se beneficiaría de la extracción de métodos para hacer que el algoritmo sea esencialmente una serie de llamadas a funciones que "hacen una sola cosa"? Un ejemplo rápido de extracción de un método podría ser mover las primeras tres declaraciones if a una función como:

private static int getTValue(int n)
    {
        int t = 1;
        if (n >= 2047) {
            t = 2;
        }
        if (n >= 1373653) {
            t = 3;
        }
        if (n >= 25326001) {
            t = 4;    
        }
        return t;
    }

Nota: Esta pregunta es diferente del posible duplicado (aunque esa pregunta también me ayuda), porque estoy tratando de determinar si entiendo la intención del autor de Clean Code y estoy proporcionando un ejemplo específico para hacer las cosas más hormigón.

1 oeste
fuente
3
por lo que puedo ver, esta función solo hace una cosa ... no tiene efectos secundarios que pueda ver. ¿Qué te hace pensar que podría no estar limpio? ¿Qué parte de esta función consideraría digna de poner en otra función para que sea más limpia?
Newtopian
14
El título de su pregunta pide una situación de "vida real", y luego su ejemplo me parece un ejemplo perfecto de una función que no es la vida real (al menos, para el 99.9% de los desarrolladores de aplicaciones o web). Puede ser una función de la vida real para los teóricos de números, matemáticos o informáticos que trabajan en ese campo específico, por supuesto.
Doc Brown
2
Sí, para mí esto es la vida real, ya que actualmente se desarrollan en el campo de la teoría algebraica de números computacional :)
1west
2
Probablemente refactorice getTFactor () como usted describe.
user949300

Respuestas:

17

El "código limpio" no es un fin en sí mismo, es un medio para un fin. El objetivo principal de refactorizar funciones más grandes en funciones más pequeñas y limpiar el código de otras maneras es mantener el código en evolución y mantenible.

Al elegir un algoritmo matemático tan específico como la prueba principal "Miller-Rabin" de un libro de texto, la mayoría de los programadores no quieren evolucionarlo. Su objetivo estándar es transferirlo del pseudocódigo del libro de texto correctamente al lenguaje de programación de su entorno. Para este propósito, recomendaría seguir el libro de texto lo más cerca posible, lo que generalmente significa no refactorizar.

Sin embargo, para alguien que trabaja como matemático en ese campo que está tratando de trabajar en ese algoritmo y cambiarlo o mejorarlo, en mi humilde opinión, dividir esta función en otras más pequeñas y bien nombradas, o reemplazar el grupo de "números mágicos" por constantes con nombre, puede ayuda a hacer cambios en el código más fácil como para cualquier otro tipo de código también.

Doc Brown
fuente
1
Esto es exactamente lo que estaba buscando. Estaba teniendo problemas para determinar cuándo usar las prácticas de código limpio en el campo en el que me estoy desarrollando. Su respuesta proporciona la claridad que estaba buscando. ¡Gracias!
1 de