Estoy enfrentando problemas con lo que siento es demasiada abstracción en la base del código (o al menos lidiar con él). La mayoría de los métodos en la base de código se han abstraído para incluir al padre A más alto en la base de código, pero el niño B de este padre tiene un nuevo atributo que afecta la lógica de algunos de esos métodos. El problema es que esos atributos no se pueden verificar en esos métodos porque la entrada se abstrae en A y, por supuesto, A no tiene este atributo. Si trato de hacer un nuevo método para manejar B de manera diferente, se llama a duplicar el código. La sugerencia de mi jefe técnico es hacer un método compartido que tome parámetros booleanos, pero el problema con esto es que algunas personas lo ven como un "flujo de control oculto", donde el método compartido tiene una lógica que puede no ser evidente para futuros desarrolladores. , y también este método compartido crecerá demasiado complejo / complicado una vez si es necesario agregar atributos futuros, incluso si se divide en métodos compartidos más pequeños. Esto también aumenta el acoplamiento, disminuye la cohesión y viola el principio de responsabilidad única, que alguien de mi equipo señaló.
Esencialmente, gran parte de la abstracción en esta base de código ayuda a reducir la duplicación de código, pero hace que extender / cambiar los métodos sea más difícil cuando se hacen para tomar la mayor abstracción. ¿Qué debo hacer en una situación como esta? Estoy en el centro de la culpa, a pesar de que todos los demás no pueden ponerse de acuerdo sobre lo que consideran bueno, así que al final me duele.
Respuestas:
No toda la duplicación de código se crea igual.
Digamos que tiene un método que toma dos parámetros y los agrega juntos llamados
total()
. Digamos que tienes otro llamadoadd()
. Sus implementaciones se ven completamente idénticas. ¿Deberían fusionarse en un método? ¡¡¡NO!!!El principio Don't-Repeat-Yourself o DRY no se trata de repetir código. Se trata de difundir una decisión, una idea, de modo que si alguna vez cambia su idea, tiene que reescribirla en todas partes para difundir esa idea. Blegh Eso es terrible. No lo hagas En su lugar, use DRY para ayudarlo a tomar decisiones en un solo lugar .
Pero DRY puede corromperse y convertirse en un hábito de escanear código en busca de una implementación similar que parezca una copia y pegue de otro lugar. Esta es la forma de cerebro seco de SECO. Demonios, podrías hacer esto con una herramienta de análisis estático. No ayuda porque ignora el punto de DRY que es mantener el código flexible.
Si mis requisitos totales cambian, es posible que tenga que cambiar mi
total
implementación. Eso no significa que deba cambiar miadd
implementación. Si algún chiflado los unió en un solo método, ahora tengo un poco de dolor innecesario.Cuanto dolor Seguramente podría copiar el código y crear un nuevo método cuando lo necesite. Así que no es gran cosa, ¿verdad? Malarky! ¡Si nada más me has costado un buen nombre! Es difícil encontrar buenos nombres y no responden bien cuando juegas con su significado. Los buenos nombres, que aclaran la intención, son más importantes que el riesgo de que haya copiado un error que, francamente, es más fácil de corregir cuando su método tiene el nombre correcto.
Por lo tanto, mi consejo es dejar de dejar que las reacciones instintivas a un código similar atan su base de códigos en nudos. No estoy diciendo que eres libre de ignorar el hecho de que existen métodos y, en cambio, copiar y pegar de todas formas. No, cada método debe tener un buen nombre que respalde la idea de la que se trata. Si su implementación coincide con la implementación de alguna otra buena idea, en este momento, hoy, ¿a quién diablos le importa?
Por otro lado, si tiene un
sum()
método que tiene una implementación idéntica o incluso diferente quetotal()
, pero cada vez que cambian sus requisitos totales, tiene que cambiar,sum()
entonces hay una buena posibilidad de que sean la misma idea con dos nombres diferentes. No solo el código sería más flexible si se fusionaran, sería menos confuso de usar.En cuanto a los parámetros booleanos, sí, eso es un olor desagradable de código. El flujo de control no solo es un problema, sino que demuestra que has cortado una abstracción en un mal momento. Se supone que las abstracciones hacen las cosas más simples de usar, no más complicadas. Pasar bools a un método para controlar su comportamiento es como crear un lenguaje secreto que decida a qué método realmente está llamando. ¡Ay! No me hagas eso. Dé a cada método su propio nombre a menos que tenga algo de honesto para el polimorfismo que está sucediendo.
Ahora, pareces agotado por la abstracción. Eso es una lástima porque la abstracción es algo maravilloso cuando se hace bien. Lo usas mucho sin pensarlo. Cada vez que conduce un automóvil sin tener que comprender el sistema de piñón y cremallera, cada vez que utiliza un comando de impresión sin pensar en las interrupciones del sistema operativo, y cada vez que se cepilla los dientes sin pensar en cada cerda individual.
No, el problema al que te enfrentas es una mala abstracción. Abstracción creada para servir a un propósito diferente a sus necesidades. Necesita interfaces simples en objetos complejos que le permitan solicitar que se satisfagan sus necesidades sin tener que comprender esos objetos.
Cuando escribe un código de cliente que usa otro objeto, sabe cuáles son sus necesidades y qué necesita de ese objeto. No lo hace. Es por eso que el código del cliente posee la interfaz. Cuando eres el cliente, nada puede decirte cuáles son tus necesidades más que tú. Pones una interfaz que muestra cuáles son tus necesidades y exiges que cualquier cosa que se te entregue satisfaga esas necesidades.
Eso es abstracción. Como cliente, ni siquiera sé con qué estoy hablando. Solo sé lo que necesito de él. Si eso significa que tienes que envolver algo para cambiar su interfaz antes de entregármelo bien. No me importa Solo haz lo que necesito hacer. Deja de hacerlo complicado.
Si tengo que mirar dentro de una abstracción para entender cómo usarla, la abstracción ha fallado. No debería necesitar saber cómo funciona. Solo que funciona. Dale un buen nombre y si miro dentro no debería sorprenderme lo que encuentro. No me hagas seguir mirando adentro para recordar cómo usarlo.
Cuando insiste en que la abstracción funciona de esta manera, la cantidad de niveles detrás de ella no importa. Mientras no estés mirando detrás de la abstracción. Insiste en que la abstracción se ajusta a sus necesidades y no se adapta a ella. Para que esto funcione, debe ser fácil de usar, tener un buen nombre y no tener fugas .
Esa es la actitud que generó la Inyección de Dependencia (o simplemente la aprobación de referencia si eres de la vieja escuela como yo). Funciona bien con la composición preferida y la delegación sobre la herencia . La actitud tiene muchos nombres. Mi favorito es decir, no preguntar .
Podría ahogarte en principios todo el día. Y parece que tus compañeros de trabajo ya lo son. Pero aquí está la cosa: a diferencia de otros campos de ingeniería, esta cosa de software tiene menos de 100 años. Todos todavía lo estamos descubriendo. Así que no dejes que alguien con un montón de libros de sonido intimidantes que te aprendan te obligue a escribir código difícil de leer. Escúchelos pero insista en que tengan sentido. No tomes nada por fe. Las personas que codifican de alguna manera solo porque se les dijo que así es sin saber por qué hacen el mayor lío de todos.
fuente
El dicho habitual que todos leemos aquí y allá es:
Bueno, esto no es cierto! Tu ejemplo lo demuestra. Por lo tanto, propondría la declaración ligeramente modificada (siéntase libre de reutilizar ;-)):
Hay dos problemas diferentes en su caso:
Ambos están correlacionados:
Shape
cálculo puedesurface()
ser especializado.Si abstrae alguna operación donde hay un patrón de comportamiento general común, tiene dos opciones:
Además, este enfoque podría dar como resultado un efecto de acoplamiento abstracto a nivel de diseño. Cada vez que desee agregar algún tipo de comportamiento especializado nuevo, deberá abstraerlo, cambiar el padre abstracto y actualizar todas las demás clases. Ese no es el tipo de propagación de cambio que uno puede desear. Y no está realmente en el espíritu de las abstracciones, no depende de la especialización (al menos en el diseño).
No conozco tu diseño y no puedo ayudar más. Quizás es realmente un problema muy complejo y abstracto y no hay mejor manera. ¿Pero cuáles son las probabilidades? Los síntomas de la sobregeneralización están aquí. ¿Puede ser el momento de mirarlo de nuevo y considerar la composición sobre la generalización ?
fuente
Cada vez que veo un método en el que el comportamiento activa el tipo de su parámetro, inmediatamente considero primero si ese método realmente pertenece al parámetro del método. Por ejemplo, en lugar de tener un método como:
Yo haría esto:
Llevamos el comportamiento al lugar que sabe cuándo usarlo. Creamos una abstracción real donde no necesita conocer los tipos o los detalles de la implementación. Para su situación, podría tener más sentido mover este método desde la clase original (que llamaré
O
) para escribirA
y anularlo en tipoB
. Si el método se llamadoIt
en algún objeto, moverdoIt
aA
y anulación con el diferente comportamiento enB
. Si hay bits de datos desde dondedoIt
se llama originalmente, o si el método se usa en suficientes lugares, puede dejar el método original y delegar:Sin embargo, podemos sumergirnos un poco más. Veamos la sugerencia de usar un parámetro booleano y veamos qué podemos aprender sobre la forma en que piensa su compañero de trabajo. Su propuesta es hacer:
Esto se parece mucho al
instanceof
que usé en mi primer ejemplo, excepto que estamos externalizando esa verificación. Esto significa que tendríamos que llamarlo de dos maneras:o:
En la primera forma, el punto de llamada no tiene idea de qué tipo
A
tiene. Por lo tanto, ¿deberíamos pasar booleanos hasta el fondo? ¿Es realmente un patrón que queremos en toda la base de código? ¿Qué sucede si hay un tercer tipo que debemos tener en cuenta? Si así es como se llama el método, deberíamos moverlo al tipo y dejar que el sistema elija la implementación para nosotros polimórficamente.En la segunda forma, ya debemos saber el tipo de
a
en el punto de llamada. Por lo general, eso significa que estamos creando la instancia allí o tomando una instancia de ese tipo como parámetro. Crear un métodoO
que tome unB
aquí funcionaría. El compilador sabría qué método elegir. Cuando estamos manejando cambios como este, la duplicación es mejor que crear la abstracción incorrecta , al menos hasta que descubramos a dónde vamos realmente. Por supuesto, sugiero que no hayamos terminado realmente sin importar lo que hayamos cambiado hasta este punto.Necesitamos mirar más de cerca la relación entre
A
yB
. En general, se nos dice que debemos favorecer la composición sobre la herencia . Esto no es cierto en todos los casos, pero es cierto en un sorprendente número de casos una vez que profundizamos.B
Hereda deA
, lo que significa que creemos queB
es unA
.B
debe usarse igual queA
, excepto que funciona un poco diferente. ¿Pero cuáles son esas diferencias? ¿Podemos dar a las diferencias un nombre más concreto? ¿NoB
es unA
, pero realmenteA
tiene unX
que podría serA'
oB'
? ¿Cómo sería nuestro código si hiciéramos eso?Si nos trasladamos en el método
A
como se sugirió anteriormente, podríamos inyectar una instancia deX
dentroA
, y delegar ese método para laX
:Podemos implementar
A'
yB'
deshacernosB
. Hemos mejorado el código dando un nombre a un concepto que podría haber sido más implícito, y nos permitimos establecer ese comportamiento en tiempo de ejecución en lugar de tiempo de compilación.A
en realidad se ha vuelto menos abstracto también. En lugar de una relación de herencia extendida, está llamando a métodos en un objeto delegado. Ese objeto es abstracto, pero está más enfocado solo en las diferencias en la implementación.Sin embargo, hay una última cosa para mirar. Volvamos a la propuesta de su compañero de trabajo. Si en todos los sitios de llamadas conocemos explícitamente el tipo
A
que tenemos, entonces deberíamos hacer llamadas como:Asumimos anteriormente al componer que
A
tiene unX
que esA'
oB'
. Pero tal vez incluso esta suposición no es correcta. ¿Es este el único lugar donde esta diferencia entreA
yB
importa? Si es así, entonces quizás podamos adoptar un enfoque ligeramente diferente. Todavía tenemos unoX
que esA'
oB'
, pero no perteneceA
. SoloO.doIt
le importa, así que solo pasémoslo aO.doIt
:Ahora nuestro sitio de llamadas se ve así:
Una vez más,
B
desaparece, y la abstracción se mueve hacia lo más enfocadoX
. Esta vez, sin embargo,A
es aún más simple al saber menos. Es aún menos abstracto.Es importante reducir la duplicación en una base de código, pero debemos considerar por qué la duplicación ocurre en primer lugar. La duplicación puede ser un signo de abstracciones más profundas que están tratando de salir.
fuente
La abstracción por herencia puede volverse bastante fea. Jerarquías de clases paralelas con fábricas típicas. Refactorizar puede convertirse en un dolor de cabeza. Y también el desarrollo posterior, el lugar donde te encuentras.
Existe una alternativa: puntos de extensión , de abstracciones estrictas y personalización escalonada. Digamos una personalización de clientes gubernamentales, basada en esa personalización para una ciudad específica.
Una advertencia: Desafortunadamente, esto funciona mejor cuando todas (o la mayoría) de las clases se hacen extendale. No hay opción para ti, tal vez en pequeño.
Esta extensibilidad funciona al tener una clase base de objeto extensible que contiene extensiones:
Internamente hay una asignación diferida de objetos a objetos extendidos por clase de extensión.
Para las clases y componentes GUI, la misma extensibilidad, en parte con herencia. Agregar botones y tal.
En su caso, una validación debería ver si está extendida y validarse contra las extensiones. La introducción de puntos de extensión solo para un caso agrega código incomprensible, no es bueno.
Por lo tanto, no hay solución sino tratar de trabajar en el contexto actual.
fuente
El 'control de flujo oculto' me suena demasiado manual.
Cualquier construcción o elemento sacado de contexto puede tener esa característica.
Las abstracciones son buenas. Los atenúo con dos pautas:
Mejor no hacer un resumen demasiado pronto. Espere más ejemplos de patrones antes de abstraer. 'Más' es, por supuesto, subjetivo y específico a la situación que es difícil.
Evite demasiados niveles de abstracción solo porque la abstracción es buena. Un programador tendrá que mantener esos niveles en su cabeza para el código nuevo o modificado a medida que sondeen la base de código y lleguen a 12 niveles de profundidad. El deseo de un código bien resumido puede llevar a tantos niveles que es difícil de seguir para muchas personas. Esto también conduce a bases de código 'ninja mantenido solo'.
En ambos casos, 'más y' demasiados 'no son números fijos. Depende. Eso es lo que lo hace difícil.
También me gusta este artículo de Sandi Metz
https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction
la duplicación es mucho más barata que la abstracción incorrecta
y
prefiere la duplicación sobre la abstracción incorrecta
fuente