¿Debo usar métodos abstractos o virtuales?

11

Si suponemos que no es deseable que la clase base sea una clase de interfaz pura, y usar los 2 ejemplos a continuación, ¿cuál es un mejor enfoque, usando la definición de clase de método abstracto o virtual?

  • La ventaja de la versión "abstracta" es que probablemente se vea más limpia y obligue a la clase derivada a dar una implementación con esperanza significativa.

  • La ventaja de la versión "virtual" es que otros módulos pueden extraerla fácilmente y usarla para probar sin agregar un montón de marco subyacente como requiere la versión abstracta.

Versión abstracta:

public abstract class AbstractVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Versión virtual:

public class VirtualVersion
{
    public virtual ReturnType Method1()
    {
        return ReturnType.NotImplemented;
    }

    public virtual ReturnType Method2()
    {
        return ReturnType.NotImplemented;
    }
             .
             .
    public virtual ReturnType MethodN()
    {
        return ReturnType.NotImplemented;
    }

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}
Remojar
fuente
¿Por qué suponemos que una interfaz no es deseable?
Anthony Pegram
Sin un problema parcial, es difícil decir que uno es mejor que el otro.
Codismo
@Anthony: una interfaz no es deseable porque hay una funcionalidad útil que también entrará en esta clase.
Dunk
44
return ReturnType.NotImplemented? ¿Seriamente? Si no puede rechazar el tipo no implementado en tiempo de compilación (puede; use métodos abstractos), al menos, arroje una excepción.
Jan Hudec
3
@Dunk: Aquí son necesarios. Los valores de retorno quedarán sin marcar.
Jan Hudec

Respuestas:

15

Mi voto, si estuviera consumiendo tus cosas, sería por los métodos abstractos. Eso va junto con "fallar temprano". Puede ser una molestia en el momento de la declaración agregar todos los métodos (aunque cualquier herramienta de refactorización decente lo hará rápidamente), pero al menos sé cuál es el problema de inmediato y lo soluciono. Prefiero eso que depurar 6 meses y el valor de los cambios de 12 personas más tarde para ver por qué de repente recibimos una excepción no implementada.

Erik Dietrich
fuente
Buen punto con respecto a la obtención inesperada del error NotImplemented. Lo cual es una ventaja en el lado abstracto, porque obtendrá un error de tiempo de compilación en lugar de tiempo de ejecución.
Dunk
3
+1: además de fallar temprano, los herederos pueden ver de inmediato qué métodos necesitan implementar a través de "Implementar clase abstracta" en lugar de hacer lo que pensaban que era suficiente y luego fallar en tiempo de ejecución.
Telastyn
Acepté esta respuesta porque fallar en el momento de la compilación fue una ventaja que no pude enumerar y debido a la referencia al uso del IDE para implementar automáticamente los métodos rápidamente.
Dunk
25

La versión virtual es propensa a errores y semánticamente incorrecta.

Abstract dice "este método no se implementa aquí. Debe implementarlo para que esta clase funcione"

Virtual dice "Tengo una implementación predeterminada pero puedes cambiarme si lo necesitas"

Si su objetivo final es la capacidad de prueba, las interfaces son normalmente la mejor opción. (esta clase hace x en lugar de esta clase es ax). Sin embargo, es posible que deba dividir sus clases en componentes más pequeños para que esto funcione bien.

Tom Squires
fuente
3

Esto depende del uso de su clase.

Si los métodos tienen una implementación razonable "vacía", tiene muchos métodos y a menudo anula solo unos pocos, entonces usar virtualmétodos tiene sentido. Por ejemplo, ExpressionVisitorse implementa de esta manera.

De lo contrario, creo que deberías usar abstractmétodos.

Idealmente, no debería tener métodos que no estén implementados, pero en algunos casos, ese es el mejor enfoque. Pero si decide hacer eso, tales métodos deberían arrojar NotImplementedException, no devolver algún valor especial.

svick
fuente
Me gustaría señalar que "NotImplementedException" a menudo indica un error de omisión, mientras que "NotSupportedException" indica una elección abierta. Aparte de eso, estoy de acuerdo.
Anthony Pegram
Vale la pena señalar que muchos métodos se definen en términos de "Haz lo que sea necesario para satisfacer las obligaciones relacionadas con X". Invocar dicho método en un objeto que no tiene elementos relacionados con X puede no tener sentido, pero aún así estaría bien definido. Además, si un objeto puede o no tener obligaciones relacionadas con X, generalmente es más limpio y más eficiente decir incondicionalmente "Satisfacer todas sus obligaciones relacionadas con X", que preguntar primero si tiene alguna obligación y pedirle condicionalmente que las satisfaga. .
supercat
1

Sugeriría que reconsidere tener una interfaz separada definida que implementa su clase base, y luego siga el enfoque abstracto.

Código de imagen como este:

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public abstract class AbstractVersion : IVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Hacer esto resuelve estos problemas:

  1. Al tener todo el código que usa objetos derivados de AbstractVersion ahora se puede implementar para recibir la interfaz IVersion, esto significa que se pueden probar más fácilmente en la unidad.

  2. La versión 2 de su producto puede implementar una interfaz IVersion2 para proporcionar funcionalidad adicional sin romper el código de sus clientes existentes.

p.ej.

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public interface IVersion2
{
    ReturnType Method2_1();
}

public abstract class AbstractVersion : IVersion, IVersion2
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();
    public abstract ReturnType Method2_1();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

También vale la pena leer acerca de la inversión de dependencias, para evitar que esta clase contenga dependencias codificadas que eviten pruebas de unidad efectivas.

Michael Shaw
fuente
Voté por proporcionar la capacidad de manejar diferentes versiones. Sin embargo, he intentado e intentado nuevamente hacer un uso efectivo de las clases de interfaz, como una parte normal del diseño y siempre termino dándome cuenta de que las clases de interfaz proporcionan un valor nulo o poco y realmente oscurecen el código en lugar de facilitar la vida. Es muy raro que tenga múltiples clases heredadas de otra, como una clase de interfaz, y no hay una buena cantidad de elementos comunes que no se puedan compartir. Las clases abstractas tienden a funcionar mejor a medida que obtengo el aspecto compartido que las interfaces no proporcionan.
Dunk
Y el uso de la herencia con buenos nombres de clase proporciona una forma mucho más intuitiva de comprender fácilmente las clases (y, por lo tanto, el sistema) que un grupo de nombres de clase de interfaz funcional que son difíciles de vincular mentalmente en su conjunto. Además, el uso de clases de interfaz tiende a crear un montón de clases adicionales, lo que hace que el sistema sea más difícil de entender de otra manera.
Dunk
-2

La inyección de dependencia depende de las interfaces. Aquí hay un breve ejemplo. Class Student tiene una función llamada CreateStudent que requiere un parámetro que implementa la interfaz "IReporting" (con un método ReportAction). Después de crear un alumno, llama a ReportAction en el parámetro de clase concreto. Si el sistema está configurado para enviar un correo electrónico después de crear un estudiante, enviamos una clase concreta que envía un correo electrónico en su implementación de ReportAction, o podríamos enviar otra clase concreta que envíe resultados a una impresora en su implementación de ReportAction. Genial para la reutilización de código.

Roger
fuente
1
Esto no parece ofrecer nada sustancial sobre los puntos hechos y explicados en las 5 respuestas anteriores
mosquito