¿Cómo refactorizar el código a algún código común?

16

Antecedentes

Estoy trabajando en un proyecto en curso de C #. No soy un programador de C #, principalmente un programador de C ++. Así que me asignaron tareas básicamente fáciles y de refactorización.

El código es un desastre. Es un gran proyecto. Como nuestro cliente exigía lanzamientos frecuentes con nuevas características y correcciones de errores, todos los demás desarrolladores se vieron obligados a adoptar un enfoque de fuerza bruta mientras codificaban. El código es muy difícil de mantener y todos los demás desarrolladores están de acuerdo con él.

No estoy aquí para debatir si lo hicieron bien. Como estoy refactorizando, me pregunto si lo estoy haciendo de la manera correcta ya que mi código refactorizado parece complejo. Aquí está mi tarea como simple ejemplo.

Problema

Hay seis clases: A, B, C, D, Ey F. Todas las clases tienen una función ExecJob(). Las seis implementaciones son muy similares. Básicamente, al principio A::ExecJob()fue escrito. Luego se requirió una versión ligeramente diferente que se implementó B::ExecJob()mediante la copia-pegado-modificación de A::ExecJob(). Cuando se requería otra versión ligeramente diferente, C::ExecJob()se escribía, etc. Las seis implementaciones tienen un código común, luego algunas líneas de código diferentes, luego nuevamente un código común y así sucesivamente. Aquí hay un ejemplo simple de las implementaciones:

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

Donde SNes un grupo de exactamente las mismas declaraciones.

Para hacerlos comunes, creé otra clase y moví el código común en una función. Usando el parámetro para controlar qué grupo de declaraciones se deben ejecutar:

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

Tenga en cuenta que este ejemplo solo emplea tres clases y declaraciones demasiado simplificadas. En la práctica, la CommonTask()función se ve muy compleja con todas esas comprobaciones de parámetros y hay muchas más declaraciones. Además, en código real, hay varias CommonTask()funciones de búsqueda.

Aunque todas las implementaciones comparten código común y las ExecJob()funciones se ven más lindas, existen dos problemas que me molestan:

  • Para cualquier cambio CommonTask(), se necesitan las seis características (y pueden ser más en el futuro) para ser probadas.
  • CommonTask()Ya es complejo. Se volverá más complejo con el tiempo.

¿Lo estoy haciendo de la manera correcta?

Donotalo
fuente
El libro de Refactorización de Martin Fowler tiene muchas técnicas específicas para refactorizar código que pueden resultarle útiles.
Allan

Respuestas:

14

¡Sí, estás absolutamente en el camino correcto!

En mi experiencia, noté que cuando las cosas son complicadas, los cambios ocurren en pequeños pasos. Lo que has hecho es el paso 1 en el proceso de evolución (o proceso de refactorización). Aquí están los pasos 2 y 3:

Paso 2

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

Este es el 'Patrón de diseño de plantilla' y está un paso adelante en el proceso de refactorización. Si la clase base cambia, las subclases (A, B, C) no necesitan verse afectadas. Puede agregar nuevas subclases con relativa facilidad. Sin embargo, de inmediato en la imagen de arriba puedes ver que la abstracción está rota. La necesidad de 'implementación vacía' es un buen indicador; muestra que hay algo mal con tu abstracción. Podría haber sido una solución aceptable a corto plazo, pero parece haber una mejor.

Paso 3

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

Este es el 'Patrón de diseño de estrategia' y parece ser una buena opción para su caso. Existen diferentes estrategias para ejecutar el trabajo y cada clase (A, B, C) lo implementa de manera diferente.

Estoy seguro de que hay un paso 4 o un paso 5 en este proceso o enfoques de refactorización mucho mejores. Sin embargo, este le permitirá eliminar el código duplicado y asegurarse de que los cambios estén localizados.

Guven
fuente
El principal problema que veo con la solución descrita en el "Paso 2" es que la implementación concreta de S5 existe dos veces.
user281377
1
Sí, ¡la duplicación de código no se elimina! Y ese es otro indicador de que la abstracción no funciona. Solo quería poner el paso 2 para mostrar cómo pienso sobre el proceso; Un enfoque paso a paso para encontrar algo mejor.
Guven
1
+1 ¡Muy buena estrategia (y no estoy hablando del patrón )!
Jordão
7

En realidad estás haciendo lo correcto. Digo esto porque:

  1. Si necesita cambiar el código para una funcionalidad de tarea común, no necesita cambiarlo en las 6 clases que contendrían el código si no lo escribe en una clase común.
  2. El número de líneas de código disminuirá.
prema
fuente
3

Verá que este tipo de código se comparte mucho con el diseño dirigido por eventos (especialmente .NET). La forma más fácil de mantener es mantener su comportamiento compartido en la menor cantidad posible.

Deje que el código de alto nivel reutilice un montón de pequeños métodos, deje el código de alto nivel fuera de la base compartida.

Tendrá mucha placa de caldera en sus implementaciones de hoja / concreto. No se asuste, está bien. Todo ese código es directo, fácil de entender. Tendrá que reorganizarlo ocasionalmente cuando las cosas se rompan, pero será fácil cambiarlo.

Verá muchos patrones en el código de alto nivel. A veces son reales, la mayoría de las veces no lo son. Las "configuraciones" de los cinco parámetros son parecidas, pero no lo son. Son tres estrategias completamente diferentes.

También quiero notar que puedes hacer todo esto con la composición y nunca preocuparte por la herencia. Tendrás menos acoplamiento.

Tom Kerr
fuente
3

Si fuera usted, probablemente agregaré 1 paso más al principio: un estudio basado en UML.

Refactorizar el código fusionando todas las partes comunes no siempre es el mejor movimiento, parece más una solución temporal que un buen enfoque.

Dibuja un esquema UML, mantén las cosas simples pero efectivas, ten en cuenta algunos conceptos básicos sobre tu proyecto como "¿qué se supone que debe hacer este software?" "¿Cuál es la mejor manera de mantener este software abstracto, modular, extensible, etc., etc.?" "¿Cómo puedo implementar la encapsulación en su mejor momento?"

Solo digo esto: no me importa el código en este momento, solo tiene que preocuparse por la lógica, cuando tiene una lógica clara en mente, todo lo demás puede convertirse en una tarea realmente fácil, al final todo este tipo de los problemas que enfrenta es simplemente causado por una mala lógica.

Micro
fuente
Este debería ser el primer paso, antes de realizar cualquier refactorización. Hasta que el código se entienda lo suficiente como para mapearse (uml o algún otro mapa de la naturaleza), la refactorización será la arquitectura en la oscuridad.
Kzqai
3

El primer paso, sin importar a dónde vaya, debería ser romper el método aparentemente grande A::ExecJob en piezas más pequeñas.

Por lo tanto, en lugar de

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

usted obtiene

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

De aquí en adelante, hay muchas formas posibles de hacerlo. Mi opinión al respecto: haga que A sea la clase base de su jerarquía de clases y ExecJob virtual y se vuelva fácil crear B, C, ... sin demasiada copia-pegado, solo reemplace ExecJob (ahora un revestimiento de cinco) con un modificado versión.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

Pero, ¿por qué tener tantas clases? Tal vez pueda reemplazarlos a todos con una sola clase que tenga un constructor al que se le pueda decir en qué acciones son necesarias ExecJob.

usuario281377
fuente
2

Estoy de acuerdo con las otras respuestas en que su enfoque está en la línea correcta, aunque no creo que la herencia sea la mejor manera de implementar un código común; prefiero la composición. De las preguntas frecuentes de C ++ que pueden explicarlo mucho mejor de lo que podría: http://www.parashift.com/c++-faq/priv-inherit-vs-compos.html

Hormiga
fuente
1

En primer lugar, usted debe asegurarse de que la herencia es realmente la herramienta aquí para el trabajo - sólo porque usted necesita un lugar común para las funciones utilizadas por sus clases Aa Fno significa que una clase base común es la cosa aquí - a veces un ayudante separada clase hace el trabajo mejor. Puede ser, puede no ser. Eso depende de tener una relación "es-a" entre A y F y su clase base común, imposible de decir por nombres artificiales AF. aquí encontrarás una publicación de blog que trata este tema.

Supongamos que decide que la clase base común es lo correcto en su caso. A continuación, la segunda cosa que me gustaría hacer es asegurarse de que los fragmentos de código S1 a S5 se implementan en cada una métodos separados S1()de S5()su clase base. Después, las funciones "ExecJob" deberían verse así:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

Como puede ver ahora, dado que S1 a S5 son solo llamadas de método, ya no hay bloques de código, la duplicación de código se ha eliminado casi por completo y ya no necesita ninguna comprobación de parámetros, evitando el problema de la creciente complejidad que podría obtener de otra manera.

Finalmente, pero solo como un tercer paso (!), Puede pensar en combinar todos esos métodos ExecJob en una de su clase base, donde la ejecución de esas partes puede controlarse mediante parámetros, tal como lo sugirió, o usando patrón de método de plantilla Debe decidir usted mismo si vale la pena el esfuerzo en su caso, según el código real.

Pero en mi humilde opinión, la técnica básica para dividir los métodos grandes en métodos pequeños es mucho, mucho más importante para evitar la duplicación de código que la aplicación de patrones.

Doc Brown
fuente