¿Hasta dónde puedo impulsar la refactorización sin cambiar el comportamiento externo?

27

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?

SiberianGuy
fuente
Si el comportamiento existente apesta o está incompleto, modifíquelo, elimínelo / vuelva a escribirlo. Es posible que no esté refactorizando en ese momento, pero a quién le importa cuál es el nombre si el sistema (¿verdad?) Mejorara como resultado de eso.
Trabajo
2
A su supervisor le puede importar si le dieron permiso para refactorizar y usted hizo una reescritura.
JeffO
2
El límite de la refactorización son las pruebas unitarias. Si tiene una especificación dibujada por ellos, ¿cualquier cosa que cambie que no rompa las pruebas está refactorizando?
George Silva
1
Pregunta similar en SO stackoverflow.com/questions/1025844/…
sylvanaar
Si desea obtener más información, este tema también es un campo de investigación activo. Hay muchas publicaciones científicas sobre este tema, por ejemplo, informatik.uni-trier.de/~ley/db/indices/a-tree/s/…
Skarab

Respuestas:

25

"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.

hay una tendencia a llamar a cualquier reproceso de código como refactorización, lo cual, supongo, es incorrecto.

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.

Entonces, ¿cuál es la palabra correcta para modificaciones que cambian el comportamiento externo?

Yo lo llamaría rediseño .

Actualizar

Muchas respuestas interesantes sobre la interfaz, pero ¿no cambiaría la refactorización de métodos para cambiar la interfaz?

¿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 Chargeclase. 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 Stringy charcampos con más apropiados enumy booleantipos. 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" ...

Péter Török
fuente
3
quisquilloso, yo diría 'cambio de diseño' no rediseño. El rediseño suena demasiado sustancial.
user606723
44
interesante - en su ejemplo de clase A es 'un órgano ya existente de código", y si el método M es público, entonces se está cambiando el comportamiento externo de un modo que probablemente se podría decir que la clase A se ha rediseñado, mientras que el sistema en su conjunto está siendo rediseñado..
saus
Me gusta observable para los usuarios. Es por eso que no diría que la ruptura de las pruebas unitarias es una señal, sino que las pruebas integrales o integrales serían una señal.
Andy Wiesendanger
8

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.

Saeed Neamati
fuente
1
No creo que sea una buena respuesta. Refactorizar puede implicar cambios de API. Pero no debería afectar al usuario final ni a la forma en que los archivos / entradas se leen / generan.
rds
3

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.

mosquito
fuente
3

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 .

Mark Seemann
fuente
3

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).

Cesar Providenti
fuente
2

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.

vpit3833
fuente
Discrepar. Si reemplaza todo su código de acceso a datos con nHibernate, no cambia el comportamiento externo, pero no sigue las "técnicas disciplinadas" de Fowler. Esto sería reingeniería y llamarlo refactorización oculta el factor de riesgo involucrado.
pdr
1

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.

Zeke Hansell
fuente
¿Qué sucede si un formulario de Windows se utiliza para abrir un cuadro de diálogo "¿Está seguro de que desea presionar el botón Aceptar?" y decidí eliminarlo porque no sirve de mucho y molesta a los usuarios, ¿luego re-factoricé el código, lo rediseñé, lo modifiqué, lo eliminé, otro?
Trabajo
@job: ha cambiado el programa para cumplir con nuevas especificaciones.
jmoreno
En mi humilde opinión, puede estar mezclando diferentes criterios aquí. Reescribir código desde cero no es una refactorización, pero esto es así independientemente de si cambió el comportamiento externo o no. Además, si cambiar una interfaz de clase no fuera refactorizar, ¿cómo es que Move Method et al. existen en el catálogo de refactorizaciones ?
Péter Török
@ Péter Török: depende completamente de lo que quieres decir con "cambiar una interfaz de clase" porque en un lenguaje OOP que implementa la herencia, la interfaz de una clase incluye no solo lo que implementa la clase en sí por todos sus ancestros. Cambiar una interfaz de clase significa eliminar / agregar un método a la interfaz (o cambiar la firma de un método, es decir, el número de un tipo de parámetros que se pasan). Refactorizar significa quién está respondiendo el método, la clase o una superclase.
Zeke Hansell
En mi humilde opinión, toda esta pregunta puede ser demasiado esotérica para ser de algún valor útil para los programadores.
Zeke Hansell
1

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
Pero si está hablando de una interfaz pública, ¿qué pasa con la refactorización del "método Move"?
SiberianGuy
@Idsa Editó mi respuesta para responder a su pregunta.
@kekela, pero aún no me queda claro dónde termina esta cosa de "refactorización"
SiberianGuy
@idsa Según la definición que ha publicado, deja de ser una refactorización en el momento en que cambia una interfaz pública. (mover un método público de una clase a otra sería un ejemplo de esto)
0

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.

Belun
fuente
Léelo varias veces, pero aún no lo entiendo
SiberianGuy
¿Qué parte no entendiste? (hay una refactorización del proceso al que se aplica su definición o refactorización de la técnica, en su ejemplo, método de movimiento, a la que no se aplica la definición; por lo tanto, el método de movimiento no rompe la definición de refactorización-la- proceso, o no cruza sus límites, cualesquiera que sean). Estoy diciendo que la preocupación que tiene no debería existir para su ejemplo. El límite de la refactorización no es algo confuso. solo estás aplicando una definición de algo a otra cosa.
Belun
0

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.

usuario34691
fuente
0

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:

  • Código fuente
  • Arquitectura del programa
  • Diseño de interfaz de usuario / interacción del usuario

y estoy seguro de varias otras cosas

Quizás refactor podría definirse como

"una acción o conjunto de acciones que disminuyen la entropía en un sistema particular"

sylvanaar
fuente
0

Definitivamente hay algunos límites sobre hasta qué punto puede refactorizar. Ver: ¿Cuándo son dos algoritmos iguales?

La gente generalmente considera los algoritmos como más abstractos que los programas que los implementan. La forma natural de formalizar esta idea es que los algoritmos son clases de equivalencia de programas con respecto a una relación de equivalencia adecuada. Argumentamos que no existe tal relación de equivalencia.

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?

Bruce Ediger
fuente
0

Puedes pensar en el significado "externo"

Externo al diario de pie.

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.

Ian
fuente