Según Martin Fowler , la refactorización del código es (el énfasis es mío):
La refactorización es una técnica disciplinada para reestructurar un cuerpo de código existente, alterando su estructura interna sin cambiar su comportamiento externo . Su corazón es una serie de pequeños comportamientos que preservan las transformaciones. Cada transformación (llamada 'refactorización') hace poco, pero una secuencia de transformaciones puede producir una reestructuración significativa. Como cada refactorización es pequeña, es menos probable que salga mal. El sistema también se mantiene funcionando completamente después de cada pequeña refactorización, lo que reduce las posibilidades de que un sistema pueda romperse gravemente durante la reestructuración.
¿Qué es el "comportamiento externo" en este contexto? Por ejemplo, si aplico la refactorización del método de movimiento y muevo algún método a otra clase, parece que cambio el comportamiento externo, ¿no?
Entonces, estoy interesado en averiguar en qué punto un cambio deja de ser refactorizador y se convierte en algo más. El término "refactorización" puede ser mal utilizado para cambios mayores: ¿hay una palabra diferente para eso?
Actualizar. Muchas respuestas interesantes sobre la interfaz, pero ¿no cambiaría la refactorización de métodos para cambiar la interfaz?
fuente
Respuestas:
"Externo" en este contexto significa "observable para los usuarios". Los usuarios pueden ser humanos en el caso de una aplicación u otros programas en el caso de una API pública.
Por lo tanto, si mueve el método M de la clase A a la clase B, y ambas clases están dentro de una aplicación, y ningún usuario puede observar ningún cambio en el comportamiento de la aplicación debido al cambio, entonces puede llamarlo correctamente refactorización.
Si, OTOH, algún otro subsistema / componente de nivel superior cambia su comportamiento o se rompe debido al cambio, eso es (generalmente) observable para los usuarios (o al menos para los registros de verificación de los administradores de sistemas). O si sus clases eran parte de una API pública, puede haber un código de terceros que depende de que M sea parte de la clase A, no B. Entonces, ninguno de estos casos es refactorizado en sentido estricto.
De hecho, es una consecuencia triste pero esperada de la refactorización que se pone de moda. Los desarrolladores han estado haciendo modificaciones de código de manera ad hoc durante años, y ciertamente es más fácil aprender una nueva palabra de moda que analizar y cambiar hábitos arraigados.
Yo lo llamaría rediseño .
Actualizar
¿De que? Las clases específicas, sí. Pero, ¿son estas clases directamente visibles para el mundo exterior de alguna manera? Si no, porque están dentro de su programa y no son parte de la interfaz externa (API / GUI) del programa , ningún cambio realizado allí es observable por partes externas (a menos que el cambio rompa algo, por supuesto).
Siento que hay una pregunta más profunda más allá de esto: ¿existe una clase específica como una entidad independiente por sí misma? En la mayoría de los casos, la respuesta es no : la clase solo existe como parte de un componente más grande, un ecosistema de clases y objetos, sin el cual no se puede instanciar y / o es inutilizable. Este ecosistema no solo incluye sus dependencias (directas / indirectas), sino también otras clases / objetos que dependen de él. Esto se debe a que sin estas clases de nivel superior, la responsabilidad asociada con nuestra clase puede no tener sentido / inútil para los usuarios del sistema.
Por ejemplo, en nuestro proyecto que se ocupa del alquiler de autos, hay un
Charge
clase. Esta clase no tiene utilidad para los usuarios del sistema por sí sola, porque los agentes de estaciones de alquiler y los clientes no pueden hacer mucho con un cargo individual: se ocupan de los contratos de contrato de alquiler en su conjunto (que incluyen un montón de diferentes tipos de cargos) . Los usuarios están principalmente interesados en la suma total de estos cargos, que deben pagar al final; el agente está interesado en las diferentes opciones de contrato, la duración del alquiler, el grupo de vehículos, el paquete de seguro, los artículos adicionales, etc., etc. seleccionados (a través de reglas comerciales sofisticadas) que rigen qué cargos están presentes y cómo se calcula el pago final fuera de estos. Y los representantes de los países / analistas comerciales se preocupan por las reglas comerciales específicas, su sinergia y sus efectos (sobre los ingresos de la empresa, etc.).Recientemente reescribí esta clase, renombrando la mayoría de sus campos y métodos (para seguir la convención de nomenclatura estándar de Java, que nuestros predecesores descuidaron por completo). También planeo refactorizaciones adicionales para reemplazar
String
ychar
campos con más apropiadosenum
yboolean
tipos. Sin duda, todo esto cambiará la interfaz de la clase, pero (si hago mi trabajo correctamente), nada de eso será visible para los usuarios de nuestra aplicación. A ninguno de ellos le importa cómo se representan los cargos individuales, aunque seguramente conocen el concepto de cargo.. Podría haber seleccionado como ejemplo otras cien clases que no representan ningún concepto de dominio, por lo que son incluso conceptualmente invisibles para los usuarios finales, pero pensé que es más interesante elegir un ejemplo donde haya al menos algo de visibilidad a nivel de concepto. Esto muestra muy bien que las interfaces de clase son solo representaciones de conceptos de dominio (en el mejor de los casos), no lo real *. La representación se puede cambiar sin afectar el concepto. Y los usuarios solo tienen y entienden el concepto; Es nuestra tarea hacer el mapeo entre concepto y representación.* Y se puede agregar fácilmente que el modelo de dominio, que representa nuestra clase, es solo una representación aproximada de algo "real" ...
fuente
Externo simplemente significa interfaz en su verdadero significado lingual. Considere una vaca para este ejemplo. Siempre que alimente algunos vegetales y obtenga leche como valor de retorno, no le importa cómo funcionan sus órganos internos. Ahora, si Dios cambia los órganos internos de las vacas, de modo que su sangre se vuelva de color azul, siempre que el punto de entrada y el punto de salida (boca y leche) no cambien, se puede considerar una refactorización.
fuente
Para mí, la refactorización ha sido más productiva / cómoda cuando los límites fueron establecidos por pruebas y / o por especificaciones formales.
Estos límites son lo suficientemente rígidos como para hacerme sentir segura sabiendo que si ocasionalmente cruzo, se detectará lo suficientemente pronto para que no tenga que revertir muchos cambios para recuperarme. Por otro lado, estos dan suficiente margen para mejorar el código sin preocuparse por cambiar el comportamiento irrelevante.
Lo que más me gusta es que este tipo de límites son adaptativos, por así decirlo. Quiero decir, 1) hago el cambio y verifico que cumpla con las especificaciones / pruebas. Luego, 2) se pasa al control de calidad o a las pruebas del usuario: tenga en cuenta que aún puede fallar porque falta algo en las especificaciones / pruebas. OK, si 3a) las pruebas pasan, ya he terminado, bien. De lo contrario, si 3b) la prueba falla, entonces 4) revierte el cambio y 5) agrega pruebas o aclara las especificaciones para que la próxima vez este error no se repita. Tenga en cuenta que no importa si la prueba pasa o falla, obtengo algo, ya sea que el código / las pruebas / las especificaciones mejoren, mis esfuerzos no se convierten en un desperdicio total.
En cuanto a los límites de otros tipos, hasta ahora no tuve mucha suerte.
"Observable para los usuarios" es una apuesta segura si uno tiene una disciplina para seguirlo, lo que para mí siempre implica mucho esfuerzo en analizar las pruebas existentes / crear nuevas, tal vez demasiado esfuerzo. Otra cosa que no me gusta de este enfoque es que seguirlo ciegamente puede resultar demasiado restrictivo. - Este cambio está prohibido porque cargar datos demorará 3 segundos en lugar de 2. - Bueno, ¿qué tal si los usuarios / expertos en experiencia de usuario comprueban si esto es relevante o no? - De ninguna manera, cualquier cambio en el comportamiento observable está prohibido, punto. ¿Seguro? usted apuesta! ¿productivo? realmente no.
Otro que probé es mantener la lógica del código (la forma en que lo entiendo al leer). Excepto por los cambios más elementales (y típicamente no muy fructíferos), este siempre fue una lata de gusanos ... ¿o debería decir una lata de errores? Me refiero a errores de regresión. Es demasiado fácil romper algo importante cuando se trabaja con código de espagueti.
fuente
El libro de Refactorización es bastante fuerte en su mensaje de que solo puede realizar la refactorización cuando tiene cobertura de prueba unitaria .
Por lo tanto, podría decir que mientras no esté rompiendo ninguna de sus pruebas unitarias, estará refactorizando . Cuando rompes las pruebas, ya no estás refactorizando.
Pero: ¿qué tal refactorizaciones tan simples como cambiar los nombres de clases o miembros? ¿No rompen las pruebas?
Sí, lo hacen, y en cada caso deberás considerar si ese descanso es significativo. Si su SUT es una API / SDK pública, entonces un cambio de nombre es, de hecho, un cambio importante. Si no, probablemente esté bien.
Sin embargo, considere que a menudo, las pruebas se rompen no porque haya cambiado el comportamiento que realmente le interesa, sino porque las pruebas son pruebas frágiles .
fuente
La mejor manera de definir el "comportamiento externo", en este contexto, puede ser "casos de prueba".
Si refactoriza el código y continúa pasando los casos de prueba (definidos antes de la refactorización), la refactorización no ha cambiado el comportamiento externo. Si uno o más casos de prueba fallan, entonces ha cambiado el comportamiento externo.
Al menos, esa es mi comprensión de los diversos libros publicados sobre el tema (por ejemplo, Fowler).
fuente
El límite sería la línea que indica quién desarrolla, mantiene y respalda el proyecto y aquellos que son sus usuarios, además de los que respaldan, mantienen y desarrollan. Entonces, para el mundo externo, el comportamiento se ve igual mientras que las estructuras internas detrás del comportamiento han cambiado.
Por lo tanto, debería estar bien mover funciones entre clases siempre que no sean las que ven los usuarios.
Siempre que el reproceso de código no cambie comportamientos externos, agregue nuevas funciones o elimine funciones existentes, supongo que está bien llamar refactorizado al reproceso.
fuente
Con el debido respeto, debemos recordar que los usuarios de una clase no son los usuarios finales de las aplicaciones que se crean con la clase, sino las clases que se implementan utilizando, ya sea llamando o heredando, la clase que se está refactorizando .
Cuando dice que "el comportamiento externo no debería cambiar", quiere decir que, en lo que respecta a los usuarios, la clase se comporta exactamente igual que antes. Puede ser que la implementación original (sin refactorizar) fuera una sola clase, y la nueva implementación (refactorizada) tiene una o más superclases sobre las que se construye la clase, pero los usuarios nunca ven el interior (la implementación) solo ver la interfaz.
Entonces, si una clase tiene un método llamado "doSomethingAmazing", no le importa al usuario si eso es implementado por la clase a la que se refiere, o por una superclase sobre la cual se construye esa clase. Todo lo que le importa al usuario es que el nuevo "doSomethingAmazing" nuevo (refactorizado) tiene el mismo resultado que el viejo "doSomethingAmazing" (no refactorizado).
Sin embargo, lo que se llama refactorización en muchos casos no es una refactorización verdadera, sino tal vez una reimplificación que se hace para hacer que el código sea más fácil de modificar para agregar alguna nueva característica. Entonces, en este caso posterior de (pseudo) refactorización, el nuevo código (refactorizado) en realidad hace algo diferente, o tal vez algo más que el anterior.
fuente
Por "comportamiento externo" principalmente está hablando de la interfaz pública, pero esto también abarca las salidas / artefactos del sistema también. (es decir, tiene un método que genera un archivo, cambiar el formato del archivo estaría cambiando el comportamiento externo)
e: Consideraría el "método de movimiento" como un cambio en el comportamiento externo. Tenga en cuenta aquí que Fowler está hablando de las bases de código existentes que se han lanzado a la naturaleza. Dependiendo de su situación, es posible que pueda verificar que su cambio no interrumpa a ningún cliente externo y continuar de manera feliz.
e2: "Entonces, ¿cuál es la palabra correcta para modificaciones que cambian el comportamiento externo?" - API Refactoring, Breaking Change, etc ... sigue refactorizando, simplemente no sigue las mejores prácticas para refactorizar una interfaz pública que ya está en estado salvaje con los clientes.
fuente
El "método de movimiento" es una técnica de refactorización, no una refactorización en sí misma. Una refactorización es el proceso de aplicar varias técnicas de refactorización a las clases. Ahí, cuando dices, apliqué "método de movimiento" a una clase, en realidad no quieres decir "Refactoré (la clase)", en realidad quieres decir "Apliqué una técnica de refactorización en esa clase". La refactorización, en su sentido más puro, se aplica al diseño, o más específicamente, a alguna parte del diseño de la aplicación que puede verse como un recuadro negro.
Se podría decir que "refactorización" utilizada en el contexto de clases, significa "técnica de refactorización", por lo tanto, "método de movimiento" no rompe la definición de refactorización del proceso. En la misma página, "refactorizar" en el contexto del diseño, no rompe las características existentes en el código, solo "rompe" el diseño (que de todos modos es su propósito).
En conclusión, "el límite", mencionado en la pregunta, se cruza si confunde (mezclar: D) refactorizar la técnica con refactorizar el proceso.
PD: Buena pregunta, por cierto.
fuente
si hablas de factorizar números, entonces estás describiendo el grupo de enteros que cuando se multiplican juntos son iguales al número inicial. Si tomamos esta definición para factorizar y la aplicamos al término de programación refactorizar, entonces refactorizar sería dividir un programa en las unidades lógicas más pequeñas, de modo que cuando se ejecutan como un programa, produzcan la misma salida (dada la misma entrada ) como el programa original.
fuente
Los límites de una refactorización son específicos de una refactorización dada. Simplemente no puede haber una respuesta y abarca todo. Una razón es que el término refactorización es en sí no específico.
Puedes refactorizar:
y estoy seguro de varias otras cosas
Quizás refactor podría definirse como
fuente
Definitivamente hay algunos límites sobre hasta qué punto puede refactorizar. Ver: ¿Cuándo son dos algoritmos iguales?
Por lo tanto, no vaya demasiado lejos, o no tendrá ninguna confianza en el resultado. Por otro lado, la experiencia dicta que a menudo puede reemplazar un algoritmo con otro y obtener las mismas respuestas, a veces más rápido. Esa es la belleza, ¿eh?
fuente
Puedes pensar en el significado "externo"
Por lo tanto, cualquier cambio en el sistema que no afecte a ninguno de los cerdos puede considerarse como refactorizador.
Cambiar una interfaz de clase no es un problema, si la clase solo es utilizada por un solo sistema construido y mantenido por su equipo. Sin embargo, si la clase es una clase pública en el marco .net que usa cada programador .net, es un asunto muy diferente.
fuente