¿Anular los métodos concretos es un olor a código?

31

¿Es cierto que anular métodos concretos es un olor a código? Porque creo que si necesita anular métodos concretos:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

se puede reescribir como

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

y si B quiere reutilizar a () en A, puede reescribirse como:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

que no requiere herencia para anular el método, ¿es eso cierto?

ggrr
fuente
44
Votados en contra porque (en los comentarios a continuación), Telastyn y Doc Brown están de acuerdo en anular, pero dan respuestas opuestas sí / no a la pregunta principal porque no están de acuerdo con el significado de "código de olor". Por lo tanto, una pregunta destinada a ser sobre el diseño del código ha sido muy asociada a una jerga. Y creo que no es necesario, ya que la pregunta específica es acerca de una técnica contra otra.
Steve Jessop
55
La pregunta no está etiquetada Java, pero el código parece ser Java. En C # (o C ++), tendría que usar la virtualpalabra clave para admitir anulaciones. Entonces, el tema se hace más (o menos) ambiguo por los detalles del lenguaje.
Erik Eidt
1
Parece que casi todas las características del lenguaje de programación terminan siendo clasificadas como "olor a código" después de suficientes años.
Brandon
2
Siento que el finalmodificador existe exactamente para situaciones como estas. Si el comportamiento del método es tal que no está diseñado para ser anulado, márquelo como final. De lo contrario, espere que los desarrolladores puedan optar por anularlo.
Martin Tuskevicius

Respuestas:

34

No, no es un código de olor.

  • Si una clase no es final, permite ser subclaseada.
  • Si un método no es definitivo, se puede anular.

Está dentro de las responsabilidades de cada clase considerar cuidadosamente si la subclasificación es apropiada y qué métodos pueden ser anulados .

La clase puede definirse a sí misma o cualquier método como final, o puede imponer restricciones (modificadores de visibilidad, constructores disponibles) sobre cómo y dónde se subclasifica.

El caso habitual para reemplazar los métodos es una implementación predeterminada en la clase base que se puede personalizar u optimizar en la subclase (especialmente en Java 8 con el advenimiento de los métodos predeterminados en las interfaces).

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

Una alternativa para anular el comportamiento es el Patrón de estrategia , donde el comportamiento se abstrae como una interfaz y la implementación se puede establecer en la clase.

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}
Peter Walser
fuente
Entiendo que, en el mundo real, anular un método concreto puede ser un olor a código para algunos. Pero, me gusta esta respuesta porque me parece más especificada.
Darkwater23
En su último ejemplo, ¿no deberían Aimplementarse DescriptionProvider?
Visto el
@Spotted: A podría implementarse DescriptionProvidery, por lo tanto, ser el proveedor de descripción predeterminado. Pero la idea aquí es la separación de preocupaciones: Anecesita la descripción (algunas otras clases también podrían hacerlo), mientras que otras implementaciones las proporcionan.
Peter Walser
2
Ok, suena más como el patrón de estrategia .
Visto el
@Spotted: tienes razón, el ejemplo ilustra el patrón de estrategia, no el patrón de decorador (un decorador agregaría comportamiento a un componente). Corregiré mi respuesta, gracias.
Peter Walser
25

¿Es cierto que anular métodos concretos es un olor a código?

Sí, en general, anular los métodos concretos es un olor a código.

Debido a que el método base tiene un comportamiento asociado que los desarrolladores generalmente respetan, cambiar eso conducirá a errores cuando su implementación haga algo diferente. Peor aún, si cambian el comportamiento, su implementación correcta anterior puede exacerbar el problema. Y en general, es bastante difícil respetar el Principio de sustitución de Liskov para métodos concretos no triviales.

Ahora, hay casos en que esto se puede hacer y es bueno. Los métodos semi-triviales se pueden anular de manera segura. Métodos de base que existen sólo como un "defecto cuerdo" tipo de comportamiento que se pretende para ser montado sobre-tienen algunos usos buenos.

Por lo tanto, esto me parece un olor: a veces bueno, generalmente malo, eche un vistazo para asegurarse.

Telastyn
fuente
29
Su explicación está bien, sin embargo, llego exactamente a la conclusión opuesta: "no, anular los métodos concretos no es un olor de código en general", es solo un olor de código cuando uno anula los métodos que no estaban destinados a ser anulados. ;-)
Doc Brown
2
@DocBrown ¡Exactamente! También veo que la explicación (y particularmente la conclusión) se opone a la declaración inicial.
Tersosauros
1
@Spotted: el contraejemplo más simple es una Numberclase con un increment()método que establece el valor en su próximo valor válido. Si lo subclasifica en EvenNumberdonde increment()agrega dos en lugar de uno (y el establecedor impone la uniformidad), la primera propuesta del OP requiere implementaciones duplicadas de cada método en la clase y su segundo requiere métodos de envoltura de escritura para llamar a los métodos en el miembro. Parte del propósito de la herencia es que las clases de niños aclaren en qué se diferencian de los padres al proporcionar solo los métodos que necesitan ser diferentes.
Blrfl
55
@DocBrown: ser un olor a código no implica que algo sea necesariamente malo, solo que es probable .
mikołak
3
@docbrown: para mí, esto coincide con "favorecer la composición sobre la herencia". Si estás sobrepasando métodos concretos, ya estás en tierra de olor pequeño por tener herencia. ¿Cuáles son las probabilidades de que estés superando algo que no deberías ser? Según mi experiencia, creo que es probable. YMMV.
Telastyn
7

si necesita anular métodos concretos, [...] puede reescribirse como

Si puede reescribirse de esa manera, significa que tiene control sobre ambas clases. Pero entonces sabes si diseñaste la clase A para que se derive de esta manera y si lo hiciste, no es un olor a código.

Si, por otro lado, no tienes control sobre la clase base, no puedes volver a escribir de esa manera. Entonces, o bien la clase fue diseñada para derivarse de esta manera, en cuyo caso simplemente siga adelante, no es un olor a código, o no lo fue, en cuyo caso tiene dos opciones: buscar otra solución por completo, o seguir adelante de todos modos , porque el trabajo triunfa sobre la pureza del diseño.

Jan Hudec
fuente
Si la clase A hubiera sido diseñada para ser derivada, ¿no tendría más sentido tener un método abstracto para expresar claramente la intención? ¿Cómo el que anula el método puede saber que el método fue diseñado de esta manera?
Visto el
@Spotted, hay muchos casos en los que se pueden proporcionar implementaciones predeterminadas y cada especialización solo necesita anular algunas de ellas, ya sea para ampliar la funcionalidad o para la eficiencia. Un ejemplo extremo son los visitantes. El usuario sabe cómo se diseñaron los métodos a partir de la documentación; tiene que estar allí para explicar lo que se espera de la anulación de cualquier manera.
Jan Hudec
Entiendo su punto de vista, aún creo que es peligroso que la única forma en que un desarrollador lo haga, para saber que un método puede ser anulado, sea leyendo el documento porque: 1) si debe ser anulado, no tengo garantía de que eso sea Estará hecho. 2) si no se debe anular, alguien lo hará para su conveniencia 3) no todos leen el documento. Además, nunca enfrenté un caso en el que necesitaba anular un método completo, sino solo partes (y terminé usando un patrón de plantilla).
Visto el
@Spotted, 1) si debe ser anulado, debería serlo abstract; pero hay muchos casos en los que no tiene que ser así. 2) si se debe no ser anulado, debe ser final(no virtual). Pero todavía hay muchos casos en los que los métodos pueden ser anulados. 3) si el desarrollador posterior no lee los documentos, se dispararán a sí mismos en el pie, pero lo harán sin importar las medidas que implemente.
Jan Hudec
Ok, entonces la divergencia de nuestro punto de vista solo radica en 1 palabra. Usted dice que cada método debe ser abstracto o final, mientras que yo digo que cada método debe ser abstracto o final. :) ¿Cuáles son estos casos cuando es necesario anular un método (y es seguro)?
Visto el
3

Primero, permítanme señalar que esta pregunta solo es aplicable en ciertos sistemas de escritura. Por ejemplo, en la tipificación estructural y de tipificación de pato sistemas - una clase simplemente tener un método con el mismo nombre y la firma podría significar que clase era tipo compatible (al menos para ese método en particular, en el caso de escribir Duck).

Esto (obviamente) es muy diferente del ámbito de los lenguajes estáticamente tipados , como Java, C ++, etc. Además de la naturaleza del tipeo fuerte , como se usa tanto en Java y C ++, como en C # y otros.


Supongo que proviene de un entorno Java, donde los métodos deben marcarse explícitamente como finales si el diseñador del contrato tiene la intención de que no se anulen. Sin embargo, en lenguajes como C #, lo contrario es lo predeterminado. Los métodos son (sin ninguna decoración, palabras clave especiales) " sellados " (versión de C # de final), y deben declararse explícitamente como virtualsi se les permitiera anularlos.

Si una API o biblioteca se ha diseñado en estos lenguajes con un método concreto que se puede anular, entonces (al menos en algunos casos) debe tener sentido que se anule. Por lo tanto, no es un olor a código anular un finalmétodo no sellado / virtual / no concreto.     (Esto supone, por supuesto, que los diseñadores de la API siguen las convenciones y marcan como virtual(C #) o marcan como final(Java) los métodos apropiados para lo que están diseñando).


Ver también

Tersosauros
fuente
En el tipeo de patos, ¿son suficientes dos clases con la misma firma de método para que sean compatibles con el tipo, o también es necesario que los métodos tengan la misma semántica, de modo que sean liskov sustituibles por alguna clase base implícita?
Wayne Conrad
2

Sí, es porque puede llevar a llamar super anti-patrón. También puede desnaturalizar el propósito inicial del método, introducir efectos secundarios (=> errores) y hacer que las pruebas fallen. ¿Usarías una clase cuyas pruebas unitarias son rojas (fallando)? No lo haré

Iré aún más lejos al decir que el código de olor inicial es cuando un método no es abstractni final. Porque permite alterar (anulando) el comportamiento de una entidad construida (este comportamiento puede perderse o modificarse). Con abstracty finalla intención es inequívoca:

  • Resumen: debe proporcionar un comportamiento
  • Final: estás no permite modificar el comportamiento

La forma más segura de agregar comportamiento a una clase existente es lo que muestra su último ejemplo: usar el patrón decorador.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}
Manchado
fuente
9
Este enfoque en blanco y negro no es realmente tan útil. Piense Object.toString()en Java ... Si esto fuera así abstract, muchas cosas predeterminadas no funcionarían, ¡y el código ni siquiera se compilaría cuando alguien no haya anulado el toString()método! Si lo fuera final, las cosas serían aún peores: no hay capacidad para permitir que los objetos se conviertan en cadenas, excepto por la forma Java. Como habla la respuesta de @Peter Walser , las implementaciones predeterminadas (como Object.toString()) son importantes. Especialmente en los métodos predeterminados de Java 8.
Tersosauros
@Tersosauros Tienes razón, si toString()fuera abstracto finalsería un dolor. Mi opinión personal es que este método no funciona y hubiera sido mejor no tenerlo (como equals y hashCode ). El propósito inicial de los valores predeterminados de Java es permitir la evolución de la API con retrocompatibilidad.
Visto el
La palabra "retrocompatibilidad" tiene muchas sílabas.
Craig
@Spotted Estoy muy decepcionado con la aceptación general de la herencia concreta en este sitio, esperaba algo mejor: / Su respuesta debería tener muchos más votos a favor
TheCatWhisperer
1
@TheCatWhisperer Estoy de acuerdo, pero por otro lado, me llevó tanto tiempo descubrir las sutiles ventajas de usar la decoración sobre la herencia de concreto (de hecho, quedó claro cuando comencé a escribir pruebas primero) que no puedes culpar a nadie por eso . Lo mejor que puede hacer es tratar de educar a los demás hasta que descubran la Verdad (broma). :)
Visto el