Probablemente lo suficiente como para recomendar "Código limpio"

Es posible que nunca podamos llegar a una definición empírica de "código bueno" o "código limpio". Esto significa que la opinión de una persona sobre la opinión de otra persona sobre el "código limpio" es necesariamente muy subjetiva. No puedo ver el libro Clean Code de Robert Martin de 2008 desde el punto de vista de otra persona, solo desde el mío.



Sin embargo, para mí, el principal problema con este libro es que muchos de los ejemplos de código son horribles .



En el tercer capítulo, Funciones, Martin da varios consejos para escribir buenas funciones. Probablemente el consejo más fuerte en este capítulo es que las funciones no deben mezclar niveles de abstracción; no deberían realizar tareas de alto o bajo nivel porque esto confunde y confunde la responsabilidad de la función. También hay otras cosas importantes en este capítulo: Martin dice que los nombres de las funciones deben ser descriptivos y consistentes, y deben ser frases verbales, y deben elegirse con cuidado. Él dice que las funciones deberían hacer solo una cosa, y hacerlo bien. Él dice que las funciones no deberían tener efectos secundarios (y da un gran ejemplo) y que los argumentos de salida deben evitarse a favor de los valores de retorno. Él dice que las funciones generalmente deben ser comandos que hacen algo,o solicitudes que son para algorespuesta , pero no ambas a la vez. Él explica SECO . Todos estos son buenos consejos, aunque un poco superficiales y básicos.



Pero hay declaraciones más dudosas en este capítulo. Martin dice que los argumentos de la bandera booleana son una mala práctica, con lo que estoy de acuerdo porque sin adornos trueo falseen el código fuente son opacos y oscuros en comparación con los explícitos, IS_SUITEo IS_NOT_SUITE... pero el razonamiento de Martin es más como un argumento booleano significa que la función hace más, de una cosa que no debería hacer.



Martin dice que debería ser posible leer un solo archivo fuente de arriba a abajo como una narración, con el nivel de abstracción en cada función disminuyendo a medida que se lee, y cada función haciendo referencia a las demás más abajo. Esto está lejos de ser universal. Muchos archivos fuente, incluso diría que la mayoría de los archivos fuente, no pueden ser claramente jerárquicos de esta manera. E incluso para aquellos que son posibles, el IDE nos permite saltar trivialmente de llamadas a funciones a implementaciones de funciones y viceversa, al igual que navegamos por sitios web. Además, ¿seguimos leyendo el código de arriba a abajo? Bueno, tal vez algunos de nosotros hacemos eso.



Y luego se pone raro. Martin dice que las funciones no deberían ser lo suficientemente grandes como para contener estructuras anidadas (convenciones y bucles); no deben sangrarse más de dos niveles. Él dice que los bloques deben tener una línea de longitud, probablemente consistente en una llamada de función. Él dice que una función ideal no tiene argumentos (¿pero todavía no tiene efectos secundarios?), Y que una función con tres argumentos es confusa y difícil de probar. Lo más extraño es que Martin afirma que una función ideal es dos o cuatro líneas de código . Este consejo se encuentra en realidad al comienzo del capítulo. Esta es la primera y más importante regla:



: . : . . , , . , . 3000 . 100 300 . 20 30 . ( ), .



[...]



Cuando Kent me mostró el código, me sorprendió lo compactas que eran todas las características. Muchas de mis funciones en los programas Swing se extendieron verticalmente durante casi kilómetros. Sin embargo, cada función en el programa Kent ocupaba solo dos, tres o cuatro líneas. Todas las características eran bastante obvias. Cada función tiene su propia historia, y cada historia te lleva naturalmente al comienzo de la siguiente historia. ¡Así de cortas deberían ser las funciones!


Todo este consejo concluye con una lista de código fuente al final del Capítulo 3. Este ejemplo de código es la refactorización preferida de Martin de la clase Java que proviene de la herramienta de prueba de código abierto FitNesse.



package fitnesse.html;

import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;

public class SetupTeardownIncluder {
  private PageData pageData;
  private boolean isSuite;
  private WikiPage testPage;
  private StringBuffer newPageContent;
  private PageCrawler pageCrawler;


  public static String render(PageData pageData) throws Exception {
    return render(pageData, false);
  }

  public static String render(PageData pageData, boolean isSuite)
    throws Exception {
    return new SetupTeardownIncluder(pageData).render(isSuite);
  }

  private SetupTeardownIncluder(PageData pageData) {
    this.pageData = pageData;
    testPage = pageData.getWikiPage();
    pageCrawler = testPage.getPageCrawler();
    newPageContent = new StringBuffer();
  }

  private String render(boolean isSuite) throws Exception {
     this.isSuite = isSuite;
    if (isTestPage())
      includeSetupAndTeardownPages();
    return pageData.getHtml();
  }

  private boolean isTestPage() throws Exception {
    return pageData.hasAttribute("Test");
  }

  private void includeSetupAndTeardownPages() throws Exception {
    includeSetupPages();
    includePageContent();
    includeTeardownPages();
    updatePageContent();
  }


  private void includeSetupPages() throws Exception {
    if (isSuite)
      includeSuiteSetupPage();
    includeSetupPage();
  }

  private void includeSuiteSetupPage() throws Exception {
    include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
  }

  private void includeSetupPage() throws Exception {
    include("SetUp", "-setup");
  }

  private void includePageContent() throws Exception {
    newPageContent.append(pageData.getContent());
  }

  private void includeTeardownPages() throws Exception {
    includeTeardownPage();
    if (isSuite)
      includeSuiteTeardownPage();
  }

  private void includeTeardownPage() throws Exception {
    include("TearDown", "-teardown");
  }

  private void includeSuiteTeardownPage() throws Exception {
    include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
  }

  private void updatePageContent() throws Exception {
    pageData.setContent(newPageContent.toString());
  }

  private void include(String pageName, String arg) throws Exception {
    WikiPage inheritedPage = findInheritedPage(pageName);
    if (inheritedPage != null) {
      String pagePathName = getPathNameForPage(inheritedPage);
      buildIncludeDirective(pagePathName, arg);
    }
  }

  private WikiPage findInheritedPage(String pageName) throws Exception {
    return PageCrawlerImpl.getInheritedPage(pageName, testPage);
  }

  private String getPathNameForPage(WikiPage page) throws Exception {
    WikiPagePath pagePath = pageCrawler.getFullPath(page);
    return PathParser.render(pagePath);
  }

  private void buildIncludeDirective(String pagePathName, String arg) {
    newPageContent
      .append("\n!include ")
      .append(arg)
      .append(" .")
      .append(pagePathName)
      .append("\n");
  }
}


Nuevamente, este es el propio código de Martin, escrito de acuerdo con sus estándares personales. Este es un ideal que se nos presenta como un estudio de caso.



En este punto, confieso que mis habilidades en Java están desactualizadas y oxidadas, casi tan desactualizadas y oxidadas como este libro, que salió en 2008. Pero incluso en 2008, ¿este código era basura ilegible?



Ignoremos importlos comodines.



Tenemos dos métodos públicos estáticos, un constructor privado y quince métodos privados. De los quince métodos privados, trece tienen efectos secundarios (cambian las variables que no se les pasaron como argumentos, por ejemplo buildIncludeDirective, que tiene efectos secundarios ennewPageContent) o llame a otros métodos que tengan efectos secundarios (por ejemplo include, qué llamadas buildIncludeDirective). Solo isTestPagey findInheritedPageparece sin los efectos secundarios. Todavía usan variables que no se les pasan ( pageDatay en testPageconsecuencia), pero parecen hacerlo sin efectos secundarios.



En este punto, puede concluir que quizás la definición de "efecto secundario" de Martin no incluye las variables miembro del objeto cuyo método acabamos de llamar. Si aceptamos esta definición, a continuación, cinco de estas variables, pageData, isSuite, testPage, newPageContentypageCrawlerse pasan implícitamente a cada llamada a un método privado, y esto se considera normal; cualquier método privado es libre de hacer lo que quiera con cualquiera de estas variables.



¡Pero esta es una suposición equivocada! Aquí está la propia definición de Martin de una parte anterior de este capítulo:



Los efectos secundarios son una mentira. Su función promete hacer una cosa, pero hace otra cosa, oculta al usuario . A veces realiza cambios inesperados en las variables de su clase, por ejemplo, les asigna los valores de los parámetros pasados ​​a la función o las variables globales del sistema. En cualquier caso, dicha función es una mentira insidiosa y maliciosa, que a menudo conduce a un tiempo no natural y otras dependencias.


¡Me encanta esta definición! Estoy de acuerdo con esta definición! ¡Esta es una definición muy útil! Estoy de acuerdo en que es malo que una función realice cambios inesperados en las variables de su propia clase.



Entonces, ¿por qué el propio código de Martin, el código "limpio", no hace nada más que esto? Es increíblemente difícil descubrir qué hace cualquiera de estos códigos, porque todos estos métodos increíblemente pequeños no hacen casi nada y funcionan únicamente a través de los efectos secundarios. Solo veamos un método privado.



private String render(boolean isSuite) throws Exception {
   this.isSuite = isSuite;
  if (isTestPage())
    includeSetupAndTeardownPages();
  return pageData.getHtml();
}


¿Por qué este método tiene el efecto secundario de establecer el valor this.isSuite? ¿Por qué no simplemente pasarlo isSuitecomo un booleano a llamadas a métodos posteriores? ¿Por qué regresamos pageData.getHtml()después de pasar tres líneas de código sin hacer nada pageData? Podríamos hacer una suposición razonable de que includeSetupAndTeardownPagestiene efectos secundarios en pageData, pero entonces ¿qué? No podemos conocer uno u otro hasta que miremos. ¿Y qué otros efectos secundarios tiene esto en otras variables miembro? La incertidumbre aumenta tanto que de repente nos preguntamos si también puede isTestPagetener efectos secundarios. (¿Qué es esa repisa? ¿Dónde están las llaves?)



Martin afirma en este mismo capítulo que tiene sentido dividir una función en funciones más pequeñas, "si puede extraerle otra función con un nombre que no sea solo una repetición de su implementación". Pero luego nos da:



private WikiPage findInheritedPage(String pageName) throws Exception {
  return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}


Nota: algunos de los aspectos negativos de este código no son culpa de Martin. Esta es una refactorización de un código ya existente que, aparentemente, no fue escrito originalmente por él. Este código ya tenía API dudosas y comportamiento dudoso, los cuales se almacenan en refactorización. En primer lugar, el nombre de la clase es SetupTeardownIncluderterrible. Es al menos una frase nominal, como todos los nombres de clase, pero es una frase verbal clásica sofocada. Este es el nombre de clase que siempre obtienes cuando trabajas en un código estrictamente orientado a objetos, donde todo debería ser una clase, pero a veces realmente solo necesitas una función simple.



En segundo lugar, los contenidos se pageDatadestruyen. A diferencia de las variables miembro ( isSuite, testPage, newPageContenty pageCrawler), que en realidad no son dueñospageDataPara cambiar. Inicialmente, el llamador externo lo pasa a los renderizadores públicos de nivel superior. El método de representación hace un gran trabajo y finalmente devuelve una cadena HTML. Sin embargo, durante este trabajo, como efecto secundario, se pageDatamodifica destructivamente (ver updatePageContent). ¿Seguramente sería preferible crear un objeto completamente nuevo PageDatacon nuestras modificaciones deseadas y dejar el original intacto? Si la persona que llama intenta usarlo pageDatapara otra cosa, puede estar muy sorprendido de lo que sucedió con su contenido. Pero así es exactamente como se comportó el código original antes de la refactorización de Martin. Conservó este comportamiento, aunque lo enterró de manera muy efectiva.



*

¿Es realmente así todo el libro?



Mayormente sí. Clean Code combina una combinación desarmadora de consejos y consejos fuertes e intemporales que son muy cuestionables o desactualizados. El libro se centra casi exclusivamente en código orientado a objetos y exige las virtudes de SOLID, excluyendo otros paradigmas de programación. Se centra en el código Java, excluyendo otros lenguajes de programación, incluso otros lenguajes orientados a objetos. Hay un capítulo sobre olores y heurística, que no es más que una lista de pistas bastante razonables para buscar en su código. Pero hay algunos capítulos en su mayoría vacíos que se centran en ejemplos que requieren mucho tiempo y están bien probados de refactorizar código Java. Hay un capítulo completo que explora los aspectos internos de JUnit (el libro fue escrito en 2008, por lo que puedes imaginar lo relevante que es ahora). El uso general de Java en el libro está muy desactualizado. Este tipo de cosas son inevitables: los libros de programación tradicionalmente se vuelven obsoletos rápidamente, pero incluso para ese momento, el código proporcionado es malo.



Hay un capítulo sobre pruebas unitarias. Este capítulo tiene muchas cosas buenas, aunque básicas, sobre cómo las pruebas unitarias deben ser rápidas, independientes y reproducibles, cómo las pruebas unitarias le permiten refactorizar con mayor seguridad su código fuente, y que las pruebas unitarias deben tener aproximadamente el mismo tamaño. como código comprobable, pero mucho más fácil de leer y entender. Luego, el autor muestra una prueba unitaria, donde, según él, hay demasiados detalles:



@Test
  public void turnOnLoTempAlarmAtThreashold() throws Exception {
    hw.setTemp(WAY_TOO_COLD);
    controller.tic();
    assertTrue(hw.heaterState());
    assertTrue(hw.blowerState());
    assertFalse(hw.coolerState());
    assertFalse(hw.hiTempAlarm());
    assertTrue(hw.loTempAlarm());
  }


y orgullosamente lo rehace:



@Test
  public void turnOnLoTempAlarmAtThreshold() throws Exception {
    wayTooCold();
    assertEquals(“HBchL”, hw.getState());
  }


Esto se hace como parte de una lección general sobre el valor de inventar un nuevo lenguaje de prueba específico de dominio para sus pruebas . Estaba tan confundido por esta declaración. ¡Usaría exactamente el mismo código para demostrar exactamente el consejo opuesto! ¡No hagas eso!



*

El autor presenta tres leyes de TDD:



. , .



. , . .



. , .



, , , 30 . , .


... pero Martin no presta atención al hecho de que dividir las tareas de programación en pequeñas piezas de treinta segundos es una pérdida de tiempo increíble en la mayoría de los casos, a menudo obviamente inútil y a menudo imposible.



*

Hay un capítulo "Objetos y estructuras de datos", donde el autor da un ejemplo de una estructura de datos:



public class Point {
  public double x;
  public double y;
}


y este ejemplo de un objeto (bueno, una interfaz para un objeto):



public interface Point {
  double getX();
  double getY();
  void setCartesian(double x, double y);
  double getR();
  double getTheta();
  void setPolar(double r, double theta);
}


El esta escribiendo:



, . , . . . , , . , .


¿Y es todo?



Sí, entendiste correctamente. ¡La definición de Martin de "estructura de datos" está en desacuerdo con la definición que todos los demás usan! El libro no dice nada a ver con la codificación pura usando lo que la mayoría de nosotros consideramos que las estructuras de datos. Este capítulo es mucho más corto de lo que espera y contiene muy poca información útil.



*

No voy a reescribir todo el resto de mis notas. Tengo demasiados, y tomaría demasiado tiempo enumerar todo lo que considero incorrecto en este libro. Me centraré en un ejemplo de código atroz más. Este es el generador de números primos del Capítulo 8:



package literatePrimes;

import java.util.ArrayList;

public class PrimeGenerator {
  private static int[] primes;
  private static ArrayList<Integer> multiplesOfPrimeFactors;

  protected static int[] generate(int n) {
    primes = new int[n];
    multiplesOfPrimeFactors = new ArrayList<Integer>();
    set2AsFirstPrime();
    checkOddNumbersForSubsequentPrimes();
    return primes;
  }

  private static void set2AsFirstPrime() {
    primes[0] = 2;
    multiplesOfPrimeFactors.add(2);
  }

  private static void checkOddNumbersForSubsequentPrimes() {
    int primeIndex = 1;
    for (int candidate = 3;
         primeIndex < primes.length;
         candidate += 2) {
      if (isPrime(candidate))
        primes[primeIndex++] = candidate;
    }
  }

  private static boolean isPrime(int candidate) {
    if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
      multiplesOfPrimeFactors.add(candidate);
      return false;
    }
    return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
  }

  private static boolean
  isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
    int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
    int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
    return candidate == leastRelevantMultiple;
  }

  private static boolean
  isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
    for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
      if (isMultipleOfNthPrimeFactor(candidate, n))
        return false;
    }
    return true;
  }

  private static boolean
  isMultipleOfNthPrimeFactor(int candidate, int n) {
   return
     candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
  }

  private static int
  smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
    int multiple = multiplesOfPrimeFactors.get(n);
    while (multiple < candidate)
      multiple += 2 * primes[n];
    multiplesOfPrimeFactors.set(n, multiple);
    return multiple;
  }
}


¿Qué es este código? ¿Cuáles son los nombres de los métodos? set2AsFirstPrime? smallestOddNthMultipleNotLessThanCandidate? ¿Debería ser un código limpio? ¿Una forma clara e inteligente de tamizar números primos?



Si esa es la calidad del código que crea este programador, en su tiempo libre, en condiciones ideales, sin la presión del desarrollo real del software de producción, ¿por qué molestarse en prestar atención al resto de su libro? ¿O sus otros libros?



*

Escribí este ensayo porque veo personas que constantemente recomiendan Clean Code. Sentí la necesidad de ofrecer anti-recomendación.



Originalmente leí Clean Code en un grupo en el trabajo. Leemos un capítulo por semana durante trece semanas.



Ahora, no desea que el grupo exprese solo un acuerdo unánime al final de cada sesión. Desea que el libro evoque algún tipo de reacción de los lectores, algunos comentarios adicionales. Y supongo que, hasta cierto punto, esto significa que el libro debería decir algo con lo que no está de acuerdo o no cubrir el tema en su totalidad, como cree que debería. Sobre esta base, el "Código limpio" era válido. Tuvimos buenas discusiones. Pudimos usar los capítulos individuales como punto de partida para una discusión más profunda de las prácticas contemporáneas actuales. Hablamos de muchas cosas que no estaban cubiertas en el libro. No estuvimos de acuerdo de muchas maneras.



¿Te recomendaría este libro? No. ¿Incluso como un texto para principiantes, incluso con todas las advertencias anteriores? No. ¿Quizás en 2008 te recomendaría este libro? ¿Puedo recomendarlo ahora como un artefacto histórico, una instantánea educativa de cómo eran las mejores prácticas de programación en 2008? No, no lo haría.



*

Entonces, la pregunta principal es ¿qué libro (s) recomendaría en su lugar? No lo sé. Sugerir en los comentarios, a menos que los haya cerrado.



All Articles