¿Son obsoletas las clases / métodos abstractos?

37

Solía ​​crear muchas clases / métodos abstractos. Entonces comencé a usar interfaces.

Ahora no estoy seguro si las interfaces no están volviendo obsoletas las clases abstractas.

¿Necesitas una clase completamente abstracta? Crea una interfaz en su lugar. ¿Necesita una clase abstracta con alguna implementación? Crea una interfaz, crea una clase. Herede la clase, implemente la interfaz. Un beneficio adicional es que algunas clases pueden no necesitar la clase padre, pero solo implementarán la interfaz.

Entonces, ¿son obsoletas las clases / métodos abstractos?

Boris Yankov
fuente
¿Qué tal si su lenguaje de programación de elección no admite interfaces? Creo recordar que este es el caso de C ++.
Bernard
77
@Bernard: en C ++, una clase abstracta es una interfaz en todo menos en el nombre. Que también puedan hacer más que interfaces 'puras' no es una desventaja.
gbjbaanb
@gbjbaanb: supongo. No recuerdo haberlos usado como interfaces, sino más bien para proporcionar implementaciones predeterminadas.
Bernard
Las interfaces son la "moneda" de las referencias a objetos. En términos generales, son el fundamento del comportamiento polimórfico. Las clases abstractas tienen un propósito diferente que deadalnix explicó perfectamente.
jiggy
44
¿No es como decir "son obsoletos los modos de transporte ahora que tenemos autos"? Sí, la mayoría de las veces, usas un auto. Pero si alguna vez necesita algo más que un automóvil o no, realmente no sería correcto decir "No necesito usar modos de transporte". Una interfaz es mucho el mismo como una clase abstracta sin ningún tipo de aplicación, y con un nombre especial, ¿no?
Jack V.

Respuestas:

111

No.

Las interfaces no pueden proporcionar una implementación predeterminada, las clases abstractas y el método sí pueden. Esto es especialmente útil para evitar la duplicación de código en muchos casos.

Esta también es una forma realmente agradable de reducir el acoplamiento secuencial. Sin clases / métodos abstractos, no puede implementar un patrón de método de plantilla. Le sugiero que mire este artículo de Wikipedia: http://en.wikipedia.org/wiki/Template_method_pattern

deadalnix
fuente
3
@deadalnix El patrón de método de plantilla puede ser bastante peligroso. Puede terminar fácilmente con código altamente acoplado y comenzar a hackear el código para extender la clase abstracta con plantilla para manejar "solo un caso más".
quant_dev
99
Esto parece más un antipatrón. Puede obtener exactamente lo mismo con la composición en una interfaz. Lo mismo se aplica a las clases parciales con algunos métodos abstractos. En lugar de forzar el código del cliente para subclasificar y anularlos, debe inyectarse la implementación . Ver: en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos
44
Esto no explica en absoluto cómo reducir el acoplamiento secuencial. Estoy de acuerdo en que existen varias maneras de lograr este objetivo, pero por favor, responda el problema del que está hablando en lugar de invocar ciegamente algún principio. Terminarás haciendo programación de culto de carga si no puedes explicar por qué esto está relacionado con el problema real.
deadalnix
3
@deadalnix: decir que el acoplamiento secuencial se reduce mejor con el patrón de plantilla es una programación de culto a la carga (usar el patrón de estado / estrategia / fábrica puede funcionar también), como es el supuesto de que siempre debe reducirse. El acoplamiento secuencial es a menudo una mera consecuencia de exponer un control de grano muy fino sobre algo, lo que significa nada más que una compensación. Eso todavía no significa que no pueda escribir un contenedor que no tenga acoplamiento secuencial usando composición. De hecho, lo hace mucho más fácil.
back2dos
44
Java ahora tiene implementaciones predeterminadas para interfaces. ¿Eso cambia la respuesta?
raptortech97
15

Existe un método abstracto para que pueda llamarlo desde su clase base pero implementarlo en una clase derivada. Entonces su clase base sabe:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

Esa es una manera razonablemente buena de implementar un agujero en el patrón del medio sin que la clase derivada conozca las actividades de configuración y limpieza. Sin métodos abstractos, tendría que confiar en que la clase derivada implementara DoTasky recordara llamar base.DoSetup()y base.DoCleanup()todo el tiempo.

Editar

Además, gracias a deadalnix por publicar un enlace al Patrón de método de plantilla , que es lo que he descrito anteriormente sin saber realmente el nombre. :)

Scott Whitlock
fuente
2
+1, prefiero tu respuesta a deadalnix '. Tenga en cuenta que puede implementar un método de plantilla de una manera más directa usando delegados (en C #):public void DoTask(Action doWork)
Joh
1
@Joh: cierto, pero eso no es necesariamente tan bueno, dependiendo de su API. Si su clase base es Fruity su clase derivada es Apple, desea llamar myApple.Eat(), no myApple.Eat((a) => howToEatApple(a)). Además, no quiere Appletener que llamar base.Eat(() => this.howToEatMe()). Creo que es más limpio simplemente anular un método abstracto.
Scott Whitlock
1
@Scott Whitlock: su ejemplo de usar manzanas y frutas es demasiado ajeno a la realidad para juzgar si es preferible la herencia a los delegados en general. Obviamente, la respuesta es "depende", por lo que deja mucho espacio para debates ... De todos modos, encuentro que los métodos de plantilla ocurren mucho durante la refactorización, por ejemplo, al eliminar el código duplicado. En tales casos, generalmente no quiero meterme con la jerarquía de tipos, y prefiero mantenerme alejado de la herencia. Este tipo de cirugía es más fácil de hacer con lambdas.
Joh
Esta pregunta ilustra más que una simple aplicación del patrón Método de plantilla: el aspecto público / no público se conoce en la comunidad C ++ como el patrón de Interfaz no virtual . Al tener una función pública no virtual envolver la virtual, incluso si inicialmente la primera no hace más que llamar a la segunda, deja un punto de personalización para la configuración / limpieza, registro, creación de perfiles, comprobaciones de seguridad, etc. que podría necesitarse más adelante.
Tony
10

No, no son obsoletos.

De hecho, existe una diferencia oscura pero fundamental entre las clases / métodos abstractos y las interfaces.

si el conjunto de clases en el que uno de estos debe usarse tiene un comportamiento común que comparten (clases relacionadas, quiero decir), entonces vaya a clases / métodos abstractos.

Ejemplo: empleado, oficial, director: todas estas clases tienen CalculateSalary () en común, use clases base abstractas. CalculateSalary () puede implementarse de manera diferente, pero hay ciertas otras cosas como GetAttendance (), por ejemplo, que tiene una definición común en la clase base.

Si sus clases no tienen nada en común (clases no relacionadas, en el contexto elegido) entre ellas, pero tiene una acción que es muy diferente en la implementación, entonces vaya a Interfaz.

Ejemplo: vaca, banco, automóvil, clases no relacionadas con telesope pero Isortable puede estar allí para clasificarlas en una matriz.

Esta diferencia generalmente se ignora cuando se aborda desde una perspectiva polimórfica. Pero personalmente siento que hay situaciones en que una es más adecuada que la otra por la razón explicada anteriormente.

WinW
fuente
6

Además de las otras buenas respuestas, existe una diferencia fundamental entre las interfaces y las clases abstractas que nadie ha mencionado específicamente, a saber, que las interfaces son mucho menos confiables y, por lo tanto, imponen una carga de prueba mucho mayor que las clases abstractas. Por ejemplo, considere este código C #:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Tengo la garantía de que solo hay dos rutas de código que necesito probar. El autor de Frobbit puede confiar en el hecho de que el frobber es rojo o verde.

Si en cambio decimos:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Ahora no sé absolutamente nada sobre los efectos de esa llamada a Frob allí. Tengo que estar seguro de que todo el código en Frobbit es robusto contra cualquier posible implementación de IFrobber , incluso implementaciones de personas que son incompetentes (malas) o activamente hostiles hacia mí o mis usuarios (mucho peor).

Las clases abstractas te permiten evitar todos estos problemas; ¡usalos, usalos a ellos!

Eric Lippert
fuente
1
El problema del que habla debe resolverse mediante el uso de tipos de datos algebraicos, en lugar de doblar las clases hasta el punto en que violen el principio abierto / cerrado.
back2dos
1
En primer lugar, nunca dije que fuera bueno o moral . Me lo pones en la boca. En segundo lugar (siendo mi punto real): no es más que una excusa pobre para una función de idioma que falta. Por último, hay una solución sin clases abstractas: pastebin.com/DxEh8Qfz . En contraste con eso, su enfoque utiliza clases anidadas y abstractas, arrojando todos menos el código que requiere la seguridad juntos en una gran bola de barro. No hay una buena razón por la cual RedFrobber o GreenFrobber deberían estar vinculados a las restricciones que desea imponer. Aumenta el acoplamiento y bloquea muchas decisiones sin ningún beneficio.
back2dos
1
Sin duda, decir que los métodos abstractos son obsoletos es incorrecto, pero afirmar, por otro lado, que deberían preferirse a las interfaces es erróneo. Son simplemente una herramienta para resolver diferentes problemas que las interfaces.
Groo
2
"existe una diferencia fundamental [...] que las interfaces son mucho menos confiables [...] que las clases abstractas". No estoy de acuerdo, la diferencia que ha ilustrado en su código se basa en las restricciones de acceso, que no creo que tengan ningún motivo para diferir significativamente entre las interfaces y las clases abstractas. Puede ser así en C #, pero la pregunta es independiente del lenguaje.
Joh
1
@ back2dos: Me gustaría señalar que la solución de Eric, de hecho, utiliza un tipo de datos algebraicos: una clase abstracta es un tipo de suma. Llamar a un método abstracto es lo mismo que la coincidencia de patrones en un conjunto de variantes.
Rodrick Chapman
4

Lo dices tú mismo:

¿Necesita una clase abstracta con alguna implementación? Crea una interfaz, crea una clase. Heredar la clase, implementar la interfaz

eso suena mucho trabajo en comparación con 'heredar la clase abstracta'. Puedes hacer el trabajo por ti mismo si te acercas al código desde una vista "purista", pero creo que ya tengo suficiente que hacer sin intentar agregar a mi carga de trabajo sin ningún beneficio práctico.

gbjbaanb
fuente
Sin mencionar que si la clase no hereda la interfaz (que no se mencionó como debería), debe escribir funciones de reenvío en algunos idiomas.
Sjoerd
Umm, el "Mucho trabajo" adicional que mencionaste es que creaste una interfaz e hiciste que las clases la implementaran, ¿eso realmente parece mucho trabajo? Además de eso, por supuesto, la interfaz le brinda la capacidad de implementar la interfaz desde una nueva jerarquía que no funcionaría con una clase base abstracta.
Bill K
4

Como comenté en la publicación @deadnix: las implementaciones parciales son un antipatrón, a pesar de que el patrón de plantilla las formaliza.

Una solución limpia para este ejemplo de wikipedia del patrón de plantilla :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

Esta solución es mejor, porque usa composición en lugar de herencia . El patrón de plantilla introduce una dependencia entre la implementación de las reglas de Monopoly y la implementación de cómo se deben ejecutar los juegos. Sin embargo, estas son dos responsabilidades completamente diferentes y no hay una buena razón para unirlas.

back2dos
fuente
+1. Como nota al margen: "Las implementaciones parciales son un antipatrón, a pesar de que el patrón de plantilla las formaliza". La descripción en wikipedia define el patrón limpiamente, solo el ejemplo de código es "incorrecto" (en el sentido de que usa la herencia cuando no es necesaria y existe una alternativa más simple, como se ilustra arriba). En otras palabras, no creo que el patrón sea el culpable, solo la forma en que las personas tienden a implementarlo.
Joh
2

No. Incluso su alternativa propuesta incluye el uso de clases abstractas. Además, como no especificó el idioma, voy a seguir adelante y decir que el código genérico es la mejor opción que la herencia frágil de todos modos. Las clases abstractas tienen ventajas significativas sobre las interfaces.

DeadMG
fuente
-1. No entiendo qué tiene que ver "código genérico vs herencia" con la pregunta. Debe ilustrar o justificar por qué "las clases abstractas tienen ventajas significativas sobre las interfaces".
Joh
1

Las clases abstractas no son interfaces. Son clases que no pueden ser instanciadas.

¿Necesitas una clase completamente abstracta? Crea una interfaz en su lugar. ¿Necesita una clase abstracta con alguna implementación? Crea una interfaz, crea una clase. Herede la clase, implemente la interfaz. Un beneficio adicional es que algunas clases pueden no necesitar la clase padre, pero solo implementarán la interfaz.

Pero entonces tendrías una clase inútil no abstracta. Se requieren métodos abstractos para completar el agujero de funcionalidad en la clase base.

Por ejemplo, dada esta clase

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

¿Cómo implementaría esta funcionalidad base en una clase que no tiene Frob()ni IsFrobbingNeeded?

configurador
fuente
1
Una interfaz tampoco puede ser instanciada.
Sjoerd
@Sjoerd: Pero necesitas una clase base con la implementación compartida; eso no puede ser una interfaz.
configurador
1

Soy un creador de servlet framework donde las clases abstractas juegan un papel esencial. Diría más, necesito métodos semi abstractos, cuando un método necesita ser anulado en un 50% de los casos y me gustaría ver una advertencia del compilador sobre que ese método no fue anulado. Resuelvo el problema agregando anotaciones. Volviendo a su pregunta, hay dos casos de uso diferentes de clases abstractas e interfaces, y hasta ahora nadie está obsoleto.

Dmitriy R
fuente
0

No creo que estén obsoletas por las interfaces, pero podrían estar obsoletas por el patrón de estrategia.

El uso principal de una clase abstracta es posponer una parte de la implementación; decir, "esta parte de la implementación de la clase se puede hacer diferente".

Desafortunadamente, una clase abstracta obliga al cliente a hacer esto a través de la herencia . Mientras que el patrón de estrategia le permitirá lograr el mismo resultado sin ninguna herencia. El cliente puede crear instancias de su clase en lugar de siempre definir las suyas, y la "implementación" (comportamiento) de la clase puede variar dinámicamente. El patrón de estrategia tiene la ventaja adicional de que el comportamiento se puede cambiar en tiempo de ejecución, no solo en tiempo de diseño, y también tiene un acoplamiento significativamente más débil entre los tipos involucrados.

Dave Cousineau
fuente
0

En general, me he enfrentado a muchos más problemas de mantenimiento asociados con interfaces puras que ABC, incluso ABC utilizados con herencia múltiple. YMMV - no sé, tal vez nuestro equipo los usó de manera inadecuada.

Dicho esto, si usamos una analogía del mundo real, ¿cuánto uso hay para interfaces puras completamente desprovistas de funcionalidad y estado? Si uso USB como ejemplo, es una interfaz razonablemente estable (creo que ahora estamos en USB 3.2, pero también ha mantenido la compatibilidad con versiones anteriores).

Sin embargo, no es una interfaz sin estado. No carece de funcionalidad. Es más como una clase base abstracta que una interfaz pura. En realidad, está más cerca de una clase concreta con requisitos funcionales y estatales muy específicos, siendo la única abstracción lo que se conecta al puerto como la única parte sustituible.

De lo contrario, sería un "agujero" en su computadora con un factor de forma estandarizado y requisitos funcionales mucho más flexibles que no harían nada por sí solo hasta que cada fabricante inventara su propio hardware para hacer que ese agujero hiciera algo, en ese momento se convierte en un estándar mucho más débil y nada más que un "agujero" y una especificación de lo que debe hacer, pero no hay una disposición central sobre cómo hacerlo. Mientras tanto, podríamos terminar con 200 formas diferentes de hacerlo después de que todos los fabricantes de hardware intenten encontrar sus propias formas de vincular la funcionalidad y el estado a ese "agujero".

Y en ese momento podríamos tener ciertos fabricantes que introducen diferentes problemas sobre otros. Si necesitamos actualizar la especificación, podríamos tener 200 implementaciones concretas de puertos USB diferentes con formas totalmente diferentes de abordar la especificación que debe actualizarse y probarse. Algunos fabricantes pueden desarrollar implementaciones estándar de facto que comparten entre sí (su clase base analógica implementando esa interfaz), pero no todas. Algunas versiones pueden ser más lentas que otras. Algunos podrían tener un mejor rendimiento pero una latencia peor o viceversa. Algunos pueden usar más batería que otros. Algunos pueden fallar y no funcionar con todo el hardware que se supone que funciona con puertos USB. Algunos pueden requerir que se conecte un reactor nuclear para operar, lo que tiende a dar a sus usuarios envenenamiento por radiación.

Y eso es lo que he encontrado, personalmente, con interfaces puras. Puede haber algunos casos en los que tengan sentido, como simplemente modelar el factor de forma de una placa base contra una caja de la CPU. Las analogías de los factores de forma son, de hecho, prácticamente apátridas y carecen de funcionalidad, como ocurre con el "agujero" analógico. Pero a menudo considero que es un gran error que los equipos consideren que de alguna manera es superior en todos los casos, ni siquiera cerca.

Por el contrario, creo que ABC resolvería muchos más casos que las interfaces si esas son las dos opciones, a menos que su equipo sea tan gigantesco que sea realmente deseable tener el equivalente analógico anterior de 200 implementaciones USB competidoras en lugar de un estándar central para mantener. En un antiguo equipo en el que estaba, tuve que luchar mucho para aflojar el estándar de codificación para permitir ABC y herencia múltiple, y principalmente en respuesta a estos problemas de mantenimiento descritos anteriormente.


fuente