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
, E
y 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 SN
es 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?
fuente
Respuestas:
¡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
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
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.
fuente
En realidad estás haciendo lo correcto. Digo esto porque:
fuente
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.
fuente
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.
fuente
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
usted obtiene
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.
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
.fuente
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
fuente
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
A
aF
no 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()
deS5()
su clase base. Después, las funciones "ExecJob" deberían verse así: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.
fuente