Pregunta

Me han utilizado para hacer algunas refactorizaciones al introducir errores de compilación. Por ejemplo, si quiero eliminar un campo de mi clase y convertirlo en un parámetro para algunos métodos, generalmente elimino el campo primero, lo que provoca un error de compilación para la clase. Luego introduciría el parámetro a mis métodos, lo que interrumpiría a los llamadores. Y así. Esto usualmente me daba una sensación de seguridad. En realidad no he leído ningún libro (todavía) sobre refactorización, pero solía pensar que esta es una forma relativamente segura de hacerlo. Pero me pregunto, ¿es realmente seguro? ¿O es una mala manera de hacer las cosas?

¿Fue útil?

Solución

Esta es una técnica común y útil para lenguajes compilados estáticamente. La versión general de lo que está haciendo podría indicarse de la siguiente manera:

  

Cuando está realizando un cambio en un módulo que podría invalidar algunos usos en los clientes de ese módulo, realice el cambio inicial de tal manera que cause un error de tiempo de compilación.

Hay una variedad de corolarios:

  • Si el significado de un método, función o procedimiento cambia y el tipo no cambia también, cambie el nombre. (Cuando haya examinado y arreglado todos los usos, probablemente volverá a cambiar el nombre).

  • Si agrega un nuevo caso a un tipo de datos o un nuevo literal a una enumeración, cambie los nombres de todos los constructores de tipos de datos existentes o los literales de enumeración. (O, si tiene la suerte de tener un compilador que verifica si el análisis de casos es exhaustivo, hay formas más fáciles).

  • Si está trabajando en un idioma con sobrecarga, no simplemente cambie una variante o agregue una nueva variante. Corres el riesgo de que la sobrecarga se resuelva silenciosamente de una manera diferente. Si usa la sobrecarga, es bastante difícil hacer que el compilador trabaje para usted de la manera que usted espera. La única forma que conozco para manejar la sobrecarga es razonar globalmente sobre todos los usos. Si su IDE no lo ayuda, debe cambiar los nombres de las variantes sobrecargadas de todas . Desagradable.

Lo que realmente estás haciendo es usar el compilador para ayudarte a examinar todos los lugares en el código que debes cambiar.

Otros consejos

Nunca confío en una compilación simple al refactorizar, el código puede compilarse, pero es posible que se hayan introducido errores.

Creo que solo será mejor escribir algunas pruebas unitarias para los métodos o clases que quieres refactorizar, luego, al ejecutar la prueba después de la refactorización, estarás seguro de que no se introdujeron errores.

No estoy diciendo que vaya a Desarrollo impulsado por pruebas, solo escriba las pruebas unitarias para obtener la confianza necesaria que necesita para refactorizar.

No veo ningún problema con ello. Es seguro, y mientras no esté realizando cambios antes de compilar, no tendrá ningún efecto a largo plazo. Además, Resharper y VS tienen herramientas que hacen que el proceso sea un poco más fácil para usted.

Utiliza un proceso similar en la otra dirección en TDD: escribe código que puede no tener los métodos definidos, lo que hace que no se compile, luego se escribe suficiente código para compilar (luego se pasan las pruebas, etc.) .)

Cuando esté listo para leer libros sobre el tema, le recomiendo a Michael Feather " Trabajando de manera efectiva con el código heredado " ;. ( Agregado por un no autor: también el libro clásico de Fowler " Refactoring " - y el Refactoring puede ser útil. )

Habla de identificar las características del código en el que está trabajando antes de hacer un cambio y hacer lo que él llama refactorización de rasguños. Eso es refectorar para encontrar las características del código y luego tirar los resultados.

Lo que estás haciendo es usar el compilador como una prueba automática. Se comprobará que su código se compila, pero no si el comportamiento ha cambiado debido a su refactorización o si hubo algún efecto secundario.

Considera esto

class myClass {
     void megaMethod() 
     {
         int x,y,z;
         //lots of lines of code
         z = mysideEffect(x)+y;
         //lots more lines of code 
         a = b + c;
     }
}

podría refactorizar la adición

class myClass {
     void megaMethod() 
     {
         int a,b,c,x,y,z;
         //lots of lines of code
         z = addition(x,y);
         //lots more lines of code
         a = addition(b,c);  
     }

     int addition(int a, b)
     {
          return mysideaffect(a)+b;
     }
}

y esto funcionaría, pero la segunda adición sería incorrecta, ya que invocó el método. Serían necesarias más pruebas además de la compilación.

Es bastante fácil pensar en ejemplos en los que la refactorización por errores del compilador falla de forma silenciosa y produce resultados no deseados.
Algunos casos que vienen a la mente: (asumiré que estamos hablando de C ++)

  • Cambiar argumentos a una función donde existen otras sobrecargas con los parámetros predeterminados. Después de la refactorización, la mejor coincidencia para los argumentos podría no ser lo que usted espera.
  • Clases que tienen operadores de conversión o constructores de argumentos únicos no explícitos. Cambiar, agregar, eliminar o cambiar argumentos a cualquiera de estos puede cambiar las mejores coincidencias a las que se llama, dependiendo de la constelación involucrada.
  • Al cambiar una función virtual y sin cambiar la clase base (o cambiar la clase base de manera diferente), las llamadas se dirigirán a la clase base.

debe confiar en los errores del compilador solo si está absolutamente seguro de que el compilador detectará todos los cambios que deben realizarse. Casi siempre tiendo a ser sospechoso sobre esto.

Me gustaría agregar a toda la sabiduría por aquí que hay un caso más en el que esto podría no ser seguro. Reflexión. Esto afecta a entornos como .NET y Java (y otros también, por supuesto). Su código se compilaría, pero aún habría errores de tiempo de ejecución cuando Reflection intentaría acceder a variables no existentes. Por ejemplo, esto podría ser bastante común si utiliza un ORM como Hibernate y olvida actualizar sus archivos XML de mapeo.

Podría ser un poco más seguro buscar los archivos de código completos para la variable específica / nombre del método. Por supuesto, puede traer muchos falsos positivos, por lo que no es una solución universal; y también puede usar concatenación de cuerdas en su reflexión, lo que también haría que esto sea inútil. Pero al menos está un poco más cerca de la seguridad.

Sin embargo, no creo que exista un método 100% a prueba de errores, aparte de revisar todo el código manualmente.

Creo que es una forma bastante común, ya que encuentra todas las referencias a esa cosa específica. Sin embargo, los IDE modernos, como Visual Studio, tienen la función Buscar todas las referencias que hace que esto sea innecesario.

Sin embargo, este enfoque tiene algunas desventajas. Para proyectos grandes, puede llevar mucho tiempo compilar la aplicación. Además, no hagas esto durante mucho tiempo (es decir, vuelve a poner las cosas a trabajar tan pronto como sea posible) y no hagas esto por más de una cosa a la vez, ya que podrías olvidar la forma correcta en que lo arreglaste. la primera vez.

Es una forma y no puede haber una afirmación definitiva sobre si es segura o insegura sin saber cómo es el código que estás refactorizando y las elecciones que haces.

Si está funcionando para usted, entonces no hay razón para cambiar solo por cambiar a sí mismo, pero cuando tenga tiempo de leerlo, los recursos aquí pueden darle nuevas ideas que tal vez desee explorar con el tiempo.

http://www.refactoring.com/

Otra opción que podría considerar, si está utilizando uno de los idiomas de dotnet, es marcar el " antiguo " método con el atributo Obsoleto, que introducirá todas las advertencias del compilador, pero aún así puede dejar el código activable si hay un código fuera de su control (si está escribiendo una API, o si no usa la opción estricta en VB.Net, por ejemplo). Usted puede entonces alegremente refactorizar, teniendo la versión obsoleta llamando a la nueva; como ejemplo:

    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    public int Login()
    {
        /* do stuff */
    }

Se convierte en:

    [ObsoleteAttribute()]
    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    [ObsoleteAttribute("Replaced by Login(username, password)")]
    public int Login()
    {
        Login(Username, Pasword);
    }

    public int Login(string username, string password)
    {
        /* do stuff */
    }

Así es como tiendo a hacerlo, de todos modos ...

Este es un enfoque común, pero los resultados pueden variar dependiendo de si su idioma es estático o dinámico.

En un lenguaje estáticamente tipado, este enfoque tiene sentido ya que cualquier discrepancia que introduzcas se detectará en el momento de la compilación. Sin embargo, los lenguajes dinámicos a menudo solo encontrarán estos problemas en el tiempo de ejecución. Estos problemas no los detectaría el compilador, sino su conjunto de pruebas; asumiendo que escribiste uno.

Me da la impresión de que está trabajando con un lenguaje estático como C # o Java, así que continúe con este enfoque hasta que encuentre algún tipo de problema importante que indique que debería hacer lo contrario.

Hago las refactorizaciones habituales, pero sigo haciendo refactorizaciones al introducir errores de compilación. Generalmente los hago cuando los cambios no son tan simples y cuando esta refactorización no es una refactorización real (estoy cambiando la funcionalidad). Esos errores del compilador me dan puntos que necesito para echar un vistazo y hacer un cambio más complicado que el cambio de nombre o parámetro.

Esto suena similar a un método absolutamente estándar utilizado en el desarrollo guiado por pruebas: escriba la prueba refiriéndose a una clase inexistente, de modo que el primer paso para hacer que la prueba pase es agregar la clase, luego los métodos, etc. . Consulte libro de Beck para ver ejemplos exhaustivos de Java.

Su método para refactorizar suena peligroso porque no tiene ninguna prueba de seguridad (o al menos no menciona que tiene alguna). Puede crear un código de compilación que no haga realmente lo que quiere, o que rompa otras partes de su aplicación.

Le sugeriría que agregue una regla simple a su práctica: realice cambios sin compilación solo en el código de prueba de unidad . De esa manera, está seguro de tener al menos una prueba local para cada modificación y está registrando la intención de la modificación en su prueba antes de realizarla.

Por cierto, Eclipse hace que " fail, stub, write " Método absurdamente fácil en Java: cada objeto inexistente está marcado para usted, y Ctrl-1 más una opción de menú le dice a Eclipse que escriba un código auxiliar compilable para usted. Me interesaría saber si otros idiomas e IDE proporcionan un soporte similar.

Es " seguro " en el sentido de que, en un lenguaje suficientemente revisado en tiempo de compilación, lo obliga a actualizar todas las referencias en vivo a lo que ha cambiado.

Todavía puede salir mal si tiene un código compilado condicionalmente, por ejemplo, si ha usado el preprocesador C / C ++. Así que asegúrese de reconstruir en todas las configuraciones posibles y en todas las plataformas, si corresponde.

No elimina la necesidad de probar sus cambios. Si ha agregado un parámetro a una función, entonces el compilador no puede decirle si el valor que proporcionó cuando actualizó cada sitio de llamada para esa función era el valor correcto. Si ha eliminado un parámetro, aún puede cometer errores, por ejemplo, cambiar:

void foo(int a, int b);

a

void foo(int a);

Luego cambia la llamada desde:

foo(1,2);

a:

foo(2);

Eso compila bien, pero está mal.

Personalmente, utilizo fallas de compilación (y enlaces) como una forma de buscar en el código las referencias en vivo a la función que estoy cambiando. Pero debes tener en cuenta que esto es solo un dispositivo que ahorra mano de obra. No garantiza que el código resultante sea correcto.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top