Demasiada abstracción que hace que el código sea difícil de extender


9

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.


10
agregar un ejemplo de código para analizar el "" problema "" ayudaría a comprender la situación mucho más
Seabizkit

Creo que hay dos principios SÓLIDOS rotos aquí. Responsabilidad única: si pasa un valor booleano a una función que supuestamente controla el comportamiento, la función ya no tendrá una responsabilidad única. El otro es el principio de sustitución de Liskov. Imagine que hay una función que toma en la clase A como parámetro. Si pasa en la clase B en lugar de A, ¿se romperá la funcionalidad de esa función?
bobek

Sospecho que el método A es bastante largo y hace más de una cosa. ¿Es ese el caso?
Rad80

Respuestas:


27

Si trato de hacer un nuevo método para manejar B de manera diferente, se llama a duplicar el código.

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 llamado add(). 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 .

El principio SECO (no te repitas) dice:

Cada conocimiento debe tener una representación única, inequívoca y autorizada dentro de un sistema.

wiki.c2.com - No te repitas

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 totalimplementación. Eso no significa que deba cambiar mi addimplementació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 que total(), 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.


Estoy totalmente de acuerdo. DRY es un acrónimo de tres letras para el eslogan de tres palabras Don't Repeat Yourself, que a su vez es un artículo de 14 páginas en la wiki . Si todo lo que hace es murmurar a ciegas esas tres letras sin leer y comprender el artículo de 14 páginas, se encontrará con problemas. También está estrechamente relacionado con Once And Only Once (OAOO) y más vagamente relacionado con Single Point Of Truth (SPOT) / Single Source Of Truth (SSOT) .
Jörg W Mittag

"Sus implementaciones se ven completamente idénticas. ¿Deberían fusionarse en un método? ¡NO!" - Lo contrario también es cierto: el hecho de que dos códigos sean diferentes no significa que no sean duplicados. Hay una gran cita de Ron Jeffries en la página wiki de OAOO : "Una vez vi a Beck declarar dos parches de código casi completamente diferentes como" duplicación ", cámbielos para que fueran duplicación, y luego elimine la duplicación recién insertada para que aparezca con algo obviamente mejor ".
Jörg W Mittag

@ JörgWMittag, por supuesto. Lo esencial es la idea. Si está duplicando la idea con un código de aspecto diferente, todavía está violando en seco.
candied_orange

Tengo que imaginar que un artículo de 14 páginas sobre no repetirse tendería a repetirse mucho.
Chuck Adams el

7

El dicho habitual que todos leemos aquí y allá es:

Todos los problemas pueden resolverse agregando otra capa de abstracción.

Bueno, esto no es cierto! Tu ejemplo lo demuestra. Por lo tanto, propondría la declaración ligeramente modificada (siéntase libre de reutilizar ;-)):

Cada problema puede resolverse utilizando EL nivel de abstracción CORRECTO.

Hay dos problemas diferentes en su caso:

  • la generalización excesiva causada al agregar cada método en el nivel abstracto;
  • La fragmentación de los comportamientos concretos que conducen a la impresión de no obtener el panorama general y sentirse perdido. Un poco como en un bucle de eventos de Windows.

Ambos están correlacionados:

  • Si abstraes un método donde cada especialización lo hace de manera diferente, todo está bien. Nadie tiene problemas para comprender que un Shapecálculo puede surface()ser especializado.
  • Si abstrae alguna operación donde hay un patrón de comportamiento general común, tiene dos opciones:

    • o bien repetirá el comportamiento común en cada especialización: esto es muy redundante; y difícil de mantener, especialmente para garantizar que la parte común se mantenga alineada entre las especializaciones:
    • utiliza algún tipo de variante del patrón de método de plantilla : esto le permite tener en cuenta el comportamiento común mediante el uso de métodos abstractos adicionales que pueden especializarse fácilmente. Es menos redundante, pero los comportamientos adicionales tienden a dividirse extremadamente. Demasiado significaría que es quizás demasiado abstracto.

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 ?


5

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:

public void sort(List values) {
    if (values instanceof LinkedList) {
        // do efficient linked list sort
    } else { // ArrayList
        // do efficient array list sort
    }
}

Yo haría esto:

values.sort();

// ...

class ArrayList {
    public void sort() {
        // do efficient array list sort
    }
}

class LinkedList {
    public void sort() {
        // do efficient linked list sort
    }
}

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 escribir Ay anularlo en tipo B. Si el método se llama doIten algún objeto, mover doIta Ay anulación con el diferente comportamiento en B. Si hay bits de datos desde donde doItse llama originalmente, o si el método se usa en suficientes lugares, puede dejar el método original y delegar:

class O {
    int x;
    int y;

    public void doIt(A a) {
        a.doIt(this.x, this.y);
    }
}

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:

public void doIt(A a, boolean isTypeB) {
    if (isTypeB) {
        // do B stuff
    } else { 
        // do A stuff
    }
}

Esto se parece mucho al instanceofque usé en mi primer ejemplo, excepto que estamos externalizando esa verificación. Esto significa que tendríamos que llamarlo de dos maneras:

o.doIt(a, a instanceof B);

o:

o.doIt(a, true); //or false

En la primera forma, el punto de llamada no tiene idea de qué tipo Atiene. 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 aen 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étodo Oque tome un Baquí 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 Ay B. 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. BHereda de A, lo que significa que creemos que Bes un A. Bdebe usarse igual que A, excepto que funciona un poco diferente. ¿Pero cuáles son esas diferencias? ¿Podemos dar a las diferencias un nombre más concreto? ¿No Bes un A, pero realmente Atiene un Xque podría ser A'o B'? ¿Cómo sería nuestro código si hiciéramos eso?

Si nos trasladamos en el método Acomo se sugirió anteriormente, podríamos inyectar una instancia de Xdentro A, y delegar ese método para la X:

class A {
    X x;
    A(X x) {
        this.x = x;
    }

    public void doIt(int x, int y) {
        x.doIt(x, y);
    }
}

Podemos implementar A'y B'deshacernos B. 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. Aen 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 Aque tenemos, entonces deberíamos hacer llamadas como:

B b = new B();
o.doIt(b, true);

Asumimos anteriormente al componer que Atiene un Xque es A'o B'. Pero tal vez incluso esta suposición no es correcta. ¿Es este el único lugar donde esta diferencia entre Ay Bimporta? Si es así, entonces quizás podamos adoptar un enfoque ligeramente diferente. Todavía tenemos uno Xque es A'o B', pero no pertenece A. Solo O.doItle importa, así que solo pasémoslo a O.doIt:

class O {
    int x;
    int y;

    public void doIt(A a, X x) {
        x.doIt(a, x, y);
    }
}

Ahora nuestro sitio de llamadas se ve así:

A a = new A();
o.doIt(a, new B'());

Una vez más, Bdesaparece, y la abstracción se mueve hacia lo más enfocado X. Esta vez, sin embargo, Aes 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.


1
Me parece que el código de ejemplo "malo" que está dando aquí es similar a lo que me inclinaría a hacer en un lenguaje que no sea OO. Me pregunto si aprendieron las lecciones equivocadas y los trajeron al mundo OO como codifican.
Baldrickk

1
@Baldrickk Cada paradigma trae sus propias formas de pensar, con sus ventajas y desventajas únicas. En Haskell funcional, la coincidencia de patrones sería el mejor enfoque. Aunque en un lenguaje como ese, algunos aspectos del problema original tampoco serían posibles.
cbojar

1
Esta es la respuesta correcta. Un método que cambia la implementación en función del tipo en el que opera debería ser un método de ese tipo.
Roman Reiner

0

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:

void f(CreditorBO creditor) {
    creditor.as(AllowedCreditorBO.class).ifPresent(allowedCreditor -> ...);
}

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.


0

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.