Cuando usa la herencia para reutilizar el código, ¿le resulta demasiado complicado que se trague los beneficios de la reutilización?

8

He estado codificando durante aproximadamente 8 años, sin embargo, todavía encuentro que la herencia es demasiado flexible y, a veces, te confunde totalmente con el código que has escrito. Un ejemplo más simple sería:

abstract class AClass {
    protected void method1() {
        if(check()) {
            do1();
        } else {
            do2();
        }
    }
    protected abstract void do1();
    protected abstract void do2();
}

La intención de la clase es que las personas puedan implementar do1 () y do2 () para que se pueda hacer alguna lógica adicional, sin embargo, a veces las personas deciden sobrecargar el método1 (), y luego las cosas se complican de inmediato.

Solo encuentro en el patrón de estrategia, el código se reutiliza bien a través de la herencia, en la mayoría de los casos, el diseñador de la clase base conoce muy bien sus subclases, y esa herencia es totalmente opcional.

Tengo una clase que es heredada por 4 clases: un IoHandler, y son subclases para el lado del servidor, el lado del cliente, el servidor perimetral, el servidor de origen, y comienza a volverme loco. Siempre estuve en la refactorización de códigos, siempre salí con ideas que creo que funcionarían y luego se demostró que no. Se dice que el cerebro humano solo puede contener 7 piezas de información una vez, ¿llevo demasiadas?

tactoth
fuente
1
¿Sabes que puedes evitar la sobrecarga de métodos específicos? Márcalo como final[si estás usando Java]. Esto efectivamente lo hace no virtual y no se puede sobrecargar. Perfecto para usar en esta circunstancia [patrón de plantilla]
Farrell
@Farrell De hecho, lo marco como 'protegido intensamente para que pueda sobrecargarse'. Creo que cuando hago esto estoy usando algunas ideas de MFC, que fue mal diseñado.
touchoth
2
La sobrecarga de métodos no tiene nada que ver con la herencia. Es realmente extraño que varios comentarios e incluso la respuesta aceptada lo repitan textualmente. ¿Seguramente estás hablando de anular ?
Aaronaught
@Aaronaught Bueno, tu corrección es realmente útil :(
tactoth

Respuestas:

3

Una vez que sigas repitiendo estos patrones, ya no parecen tan complicados.

Además, debe conocer las clases base antes de heredarlas. Y la herencia no será opcional mientras se reutilice el código común. Tome cualquier otro patrón como fábrica abstracta / compuesto: esa herencia en realidad tiene un propósito.

Regla general: no use la herencia solo para mantener un montón de clases juntas. Úselo donde tenga sentido. En el ejemplo artificial, el usuario de la clase secundaria que está sobrecargando el método1 debe tener alguna razón seria para hacerlo. (OTOH, también he querido una forma de evitar la sobrecarga de ciertas funciones solo, para evitar esto). Y antes de usar esa clase infantil, también debe conocer esta peculiaridad. Pero un patrón de estrategia pura no lo hará; si lo hace, hágala muy bien.

Si da su ejemplo de código real, puede haber personas aquí que puedan revisarlo por usted.

Subu Sankara Subramanian
fuente
Gracias por la respuesta, sigo pensando que la herencia se usa principalmente para compartir la lógica, siempre he estado aplicando el principio de que cuando los códigos pueden compartirse, deberían compartirse, y cuando decides no compartirlos, ves que se vuelve más claro .
touchoth
El código de ejemplo es demasiado grande, tengo que resolver más de 30 clases.
tactoth
18

Sí, puede ser muy complicado ... y estás en buena compañía.

Joshua Bloch en Java efectivo captura muy bien algunos de los problemas de herencia y recomienda favorecer la composición en lugar de la herencia.

La herencia es una forma poderosa de lograr la reutilización del código, pero no siempre es la mejor herramienta para el trabajo. Usado de manera inapropiada, conduce a software frágil. Es seguro usar la herencia dentro de un paquete, donde la implementación de la subclase y la superclase están bajo el control de los mismos programadores. También es seguro usar la herencia cuando se extienden clases específicamente diseñadas y documentadas para la extensión (Ítem 15). Sin embargo, heredar clases concretas ordinarias a través de los límites del paquete es peligroso. Como recordatorio, este libro usa la palabra "herencia" para referirse a la herencia de implementación (cuando una clase extiende a otra). Los problemas discutidos en este ítem no se aplican a la herencia de la interfaz (cuando una clase implementa una interfaz o cuando una interfaz extiende a otra).

A diferencia de la invocación de métodos, la herencia rompe la encapsulación. En otras palabras, una subclase depende de los detalles de implementación de su superclase para su correcto funcionamiento. La implementación de la superclase puede cambiar de una versión a otra, y si lo hace, la subclase puede romperse, aunque su código no haya sido tocado. Como consecuencia, una subclase debe evolucionar en tándem con su superclase, a menos que los autores de la superclase la hayan diseñado y documentado específicamente para su extensión.

Dakotah North
fuente
+1. Y tenga en cuenta que la idea ha existido mucho más tiempo y está mucho más extendida que solo Block and Effective Java ; ver aquí .
Josh Kelley
1

Creo que tu pregunta es demasiado vaga y general. Lo que describe parece un problema complejo, con o sin herencia. Entonces, en mi humilde opinión, es completamente normal que estés luchando con eso.

Por supuesto, en algunos casos, la herencia no es la mejor herramienta para un trabajo (referencia obligatoria a "favorecer la composición sobre la herencia" ;-). Pero cuando se usa con cuidado, me resulta útil. Según su descripción, es difícil decidir si la herencia es o no la herramienta adecuada para su caso.

Péter Török
fuente
Es una pregunta vaga y la intención es recopilar algunas ideas para facilitar la toma de decisiones de diseño. He estado cambiando entre composición y herencia de un lado a otro.
touchoth
1

si sus métodos son demasiado complejos, hace que las subclases lloren

haz que tus métodos hagan una cosa específica

Steven A. Lowe
fuente
1

La herencia de composición reduce el vector de cambio.

Imagine que su solución está implementada y utiliza AClass en 300 lugares diferentes de la base del código, y ahora es necesario cambiar 45 de las llamadas al método1 () para que se llame al método do3 () en lugar de do2 ().

Puede implementar do3 () en la interfaz IAnInterface (es de esperar que todas las implementaciones de la interfaz puedan manejar un método do3 () fácilmente) y crear un BClass que llame a do1 () o do3 () en la llamada method1 () y cambie las 45 referencias a tanto la clase AC como la dependencia inyectada.

ahora imagen que implementa este mismo cambio para do4 (), do5 (), ...

O podrías hacer lo siguiente ...

interface IFactoryCriteria {
    protected boolean check();
}

public final class DoerFactory() {
    protected final IAnInterfaceThatDoes createDoer( final IFactoryCriteria criteria ) {
        return criteria.check() ? new Doer1() : new Doer2();
    }
}

interface IAnInterfaceThatDoes {
    abstract void do();
}

public final class AClass implements IFactoryCriteria {
    private DoerFactory factory;
    public AClass(DoerFactory factory)
    {
        this.factory = factory;
    }

    protected void method1() {
        this.factory.createDoer( this ).do();
    }

    protected boolean check() {
        return true;//or false
    }
}

Ahora solo necesita implementar una segunda fábrica que devuelva Doer1 o Doer2 y cambiar la inyección de fábrica en 45 lugares. AClass no cambia, IAnInterface no cambia y puede manejar fácilmente más cambios como do4 (), do5 ().

Heath Lilley
fuente
1
+1 Me gusta esta implementación del problema sobre la propuesta por Ed. Creo que esto hace que el problema sea más SRP, además de limitar las dependencias que tiene AClass. Supongo que mi pregunta principal sería las ventajas y desventajas de pasar IAnInterfaceThatDoes a AClass en lugar de una clase de fábrica. Por ejemplo, ¿qué sucede si la lógica para determinar el IAnInterfaceThatDoes correcto queda fuera del alcance de la fábrica?
dreza
Hice que la fábrica dependiera solo para minimizar la exposición de IAnInterfaceThatDoes. Si el código de llamada necesitaba el acceso a esa interfaz, entonces ciertamente podría moverlo. No sé de una situación en la que me gustaría mover la creación de la interfaz fuera de la fábrica. Diría que si eso tiene que cambiar, entonces implementar una interfaz DoerFactoryI con el método createDoer expuesto sería otra forma de hacerlo. Entonces podría implementar una fábrica usando un marco DI o algo así.
Heath Lilley
0

Por qué no hacer:

interface IAnInterface {
    abstract void do1();
    abstract void do2();
}

public final class AClass {
    private IAnInterface Dependent;
    public AClass(IAnInterface dependent)
    {
        this.Dependent = dependent;
    }

    protected void method1() {
        if(check()) {
            this.Dependent.do1();
        } else {
            this.Dependent.do2();
        }
    }
}

(Creo que el final es el equivalente de "sellado" en C #, alguien corríjame si esa sintaxis es incorrecta).

La inyección de dependencia / IoC significa que puede heredar solo los métodos que realmente desea que las personas puedan cambiar, lo que creo que resuelve su problema. Además, no parece que la clase principal realmente haga lo que sea do1 y do2, más que delega las llamadas en sus subclases en función de una condición: ¡está rompiendo la separación de las preocupaciones!

Ed James
fuente