Cómo hacer cumplir la implementación de la interfaz para que se comporte de cierta manera

8

Supongamos que tiene la siguiente interfaz

public interface IUserRepository
{
    User GetByID(int userID);
}

¿Cómo obligaría a los implementadores de esta interfaz a lanzar una excepción si no se encuentra un usuario?

Sospecho que no es posible hacerlo solo con el código, entonces, ¿cómo obligaría a los implementadores a implementar el comportamiento deseado? ¿Ya sea a través de código, documentación, etc.?

En este ejemplo, se espera que la implementación concreta arroje un UserNotFoundException

public class SomeClass
{
    private readonly IUserRepository _userRepository;

    public SomeClass(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void DisplayUser()
    {
        try 
        {
            var user = _userRepository.GetByID(5);
            MessageBox.Show("Hello " + user.Name);
        }
        catch (UserNotFoundException)
        {
            MessageBox.Show("User not found");
        }
    }
}
Mateo
fuente
3
Esta es realmente una pregunta muy interesante. Tengo la sensación de que la respuesta correcta podría involucrar Atributos o un hack de IL. En general, supongo que intentarlo es una elección incorrecta, porque cada implementador debería ser capaz de lanzar sus propios tipos de excepciones, pero aún es concebiblemente útil. ¿Tal vez solo usarías una clase abstracta con un método de plantilla?
Magus
15
La tecnología no resuelve los problemas de las personas. Si las personas violan la interfaz, golpéenlas con un palo hasta que se detengan.
Telastyn
1
@Telastyn Quizás debería crear un programa que supere a las personas con palos.
Mateo
2
@Doval: Entonces, ¿cómo sabe la interfaz si existe un usuario válido o no sin una implementación que lo respalde?
Robert Harvey
1
@matthew: entonces su programa tendría que inferir "hasta que se detengan" correctamente, lo que lleva de nuevo a todo el problema de detención (si su programa de manejo de bastón podría decir que las personas estaban violando su interfaz o no, no lo necesitaría) .
Telastyn

Respuestas:

9

Esta es una característica del lenguaje que se omitió intencionalmente de C # . En pocas palabras, es completamente posible IUserRepository.GetByIDque falle por alguna otra razón que no sea que no se encuentre al usuario, por lo que no desea exigir un error específico cuando no puede suceder. Tiene dos opciones si, por cualquier motivo, desea aplicar este comportamiento:

  1. Defina la Userclase para que arroje la excepción si se inicializa incorrectamente.
  2. Escriba pruebas unitarias para IUserRepositoryesa prueba explícita de este comportamiento.

Tenga en cuenta que ninguna de esas opciones es "incluirla en la documentación". Idealmente, debería hacerlo de todos modos, especialmente porque la documentación es donde puede explicar por qué desea aplicar un tipo de error en particular.

DougM
fuente
2
Incluso con excepciones marcadas, no puede obligar a los implementadores a lanzar la excepción correcta en el momento correcto.
svick
7

No hay forma de requerir una implementación para lanzar una excepción a través de una interfaz, incluso en lenguajes como Java donde puede declarar que un método podría arrojar una excepción.

Puede haber una manera de asegurar (hasta cierto punto, pero no del todo) que se produce una excepción. Puede crear una implementación abstracta de su interfaz. Luego puede implementar el GetUsermétodo como final en la clase abstracta y usar el patrón de estrategia para llamar a otro miembro protegido de la subclase y lanzar una excepción si devuelve algo que no sea un usuario válido (como nulo). Esto todavía puede caerse si, por ejemplo, el otro desarrollador devuelve un tipo de objeto nulo User, pero realmente tendrían que trabajar para subvertir la intención aquí. También podrían simplemente reimplementar su interfaz, también mala, por lo que puede considerar reemplazar la interfaz por completo con la clase abstracta.

(Se pueden lograr resultados similares usando la delegación en lugar de subclasificar con algo como un decorador de envoltura).

Otra opción podría ser crear un conjunto de pruebas de conformidad que todo el código de implementación debe pasar para ser incluido. La eficacia de esto depende de cuánto control tenga sobre el enlace del otro código al suyo.

También estoy de acuerdo con otros en que la documentación y la comunicación son claras cuando se espera un requisito como este, pero no se puede aplicar por completo en el código.


Ejemplos de código:

Método de subclase:

public abstract class ExceptionalUserRepository : IUserRepository
{
    public sealed User GetUser(int user_id)
    {
        User u = FindUserByID(user_id);
        if(u == null)
        {
            throw new UserNotFoundException();
        }
        return u;
    }

    // subclasses implement this method instead
    protected abstract User FindUserByID(int user_id);
    // More code here
}

Método decorador:

public sealed class DecoratedUserRepository : IUserRepository
{
    private readonly IUserRepository _userRepository;

    public DecoratedUserRepository(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public User GetUser(int user_id)
    {
        User u = _userRepository.GetUser(user_id);
        if(u == null)
        {
            throw new UserNotFoundException();
        }
        return u;
    }

    // More code here
}

public class SomeClass
{
    private readonly IUserRepository _userRepository;

    // They now *have* to pass in exactly what you want
    public SomeClass(DecoratedUserRepository userRepository)
    {
        _userRepository = userRepository;
    }
    // More code
}

Un último punto rápido que quiero señalar que olvidé antes es que al hacer cualquiera de estos, se está vinculando a una implementación más específica, lo que significa que los desarrolladores implementadores obtienen mucha menos libertad.

cbojar
fuente
3

Si trabaja junto con los desarrolladores que desea implementar el comportamiento de lanzamiento de excepciones, entonces podría escribir pruebas unitarias (para ellos) para verificar si se produce una excepción si intenta obtener un usuario que no existe.

Por lo tanto, necesita saber qué métodos probar. No estoy familiarizado con C #, pero tal vez podría usar la reflexión para buscar todas las clases que implementan el IUserRepository y probarlas automáticamente, por lo que incluso si un desarrollador agrega otra clase, se probará cuando se ejecuten las pruebas.

Pero eso es solo lo que podría hacer en la práctica si realmente sufre de los desarrolladores que hacen mal las implementaciones con las que tiene que trabajar. En teoría, las interfaces no son para definir cómo se deben implementar las lógicas comerciales.

Espero otras respuestas ya que esta es realmente una pregunta interesante.

valenterry
fuente
3

No puedes Eso es lo que pasa con las interfaces: permiten que cualquiera las implemente en cualquier momento. Hay un número infinito de implementaciones potenciales para su interfaz, y no puede forzar a ninguna de ellas a ser correcta. Al elegir una interfaz, ha perdido su derecho de exigir que se use una implementación particular en todo el sistema. Diablos, lo que tienes ni siquiera es una interfaz, es una función. Estás escribiendo código que pasa funciones.

Si necesita seguir esta ruta, simplemente tendrá que documentar claramente la especificación que las implementaciones deben cumplir y confiar en que nadie romperá esa especificación deliberadamente.

La alternativa es tener un tipo de datos abstracto, que se traduce en una clase en lenguajes similares a Java / C #. El problema es que los idiomas principales tienen un soporte débil para los ADT y no se puede cambiar la implementación de la clase sin el sistema de archivos tomfoolery. Pero si está dispuesto a vivir con eso, puede asegurarse de que solo haya una implementación en el sistema. Eso es un paso adelante de las interfaces, donde su código puede romperse en cualquier momento, y cuando lo haga, tendrá que averiguar qué implementación de la interfaz rompió su código y de dónde vino.

EDITAR : Tenga en cuenta que incluso los hacks de IL no le permitirán garantizar la corrección de ciertos aspectos de una implementación. Por ejemplo, se supone implícitamente que GetByIDdebe devolver un valor o lanzar una excepción. Alguien podría escribir una implementación que simplemente entra en un bucle sin fin y no lo hace. No puede probar durante el tiempo de ejecución que el código se repite para siempre; si logra hacer esto, ha resuelto el problema de detención. Es posible que pueda detectar casos triviales (por ejemplo, reconocer a while (true) {}) pero no un fragmento de código arbitrario.

Doval
fuente
Bueno, una interfaz evita algunas implementaciones incorrectas: las que están mal escritas (no coinciden con las firmas de métodos en la interfaz). No puede expresar la restricción que OP quiere, pero pocos sistemas de tipos pueden hacerlo.
@delnan Si un fragmento de código tiene un tipo diferente, ¡difícilmente se puede llamar implementación! Pero estamos discutiendo la semántica. Entiendo lo que dices.
Doval
Otra cosa: un "tipo de datos abstractos" es una noción informal muy parecida a la documentación; para que sea comprobable por máquina, debe construir un sistema de tipos y usar el equivalente de interfaces de ese sistema de tipos.
@delnan Eso es un error. Ver Comprender la abstracción de datos, revisitada . Un ADT extrae datos como una interfaz, sí. Pero un ADT lo hace al ocultar la identidad de un tipo existente. Esto es equivalente a una clase con campos privados en OOP convencional. El tipo oculto es el registro cuyos campos ha hecho privados. Una interfaz, por otro lado, es un conjunto de funciones, y el tipo de implementación no aparece en ninguna parte de las firmas. Esto es lo que permite múltiples implementaciones y sustituibilidad.
Doval
Este parece ser un uso bastante diferente del "tipo de datos abstractos" del que estoy acostumbrado; Lo que yo y muchos otros conocemos como tipo de datos abstractos es solo una herramienta para hablar sobre los tipos de datos de una manera independiente del lenguaje. Lo que describe parece ser una (s) clase (s) de características del sistema que interpretan e implementan la noción antes mencionada de tipos de datos abstractos.
2

No hay forma de forzar al 100% el comportamiento que desea, pero al usar contratos de código se podría decir que las implementaciones no deben devolver un valor nulo, y tal vez que el objeto Usuario haya pasado la identificación (por lo que un objeto con ID de usuario 0 fallaría si se pasa 1 ) Además de eso, documentar lo que espera de las implementaciones también podría ayudar.

Andy
fuente
0

Probablemente sea muy tarde para responder esta pregunta, pero me gustaría compartir mi opinión al respecto. Como todas las respuestas sugieren, no es posible forzar la restricción con la interfaz. Incluso con hacks sería difícil.

Una forma de lograr esto, probablemente específica para .NET es diseñar la interfaz como se muestra a continuación:

public interface IUserRepository
{
    Task<User> GetByID(int userId);
}

La tarea no obligará a lanzar cierto tipo de Excepción, pero tiene la disposición de transmitir la intención de todo el implementador de esta interfaz. Las tareas en .NET tienen cierto patrón de uso, como Task.Result para acceder al resultado, Task.Exception si hay una excepción. Además, esto también lo usará el usuario de forma asíncrona.

Bhalchandra K
fuente
0

No puede hacer eso con una interfaz, ya que muchos otros ya han respondido.
Pero puede hacerlo con una clase base abstracta:

public abstract class UserRepositoryBase
{
  public User GetByID (int userID)  // not virtual because you don't want to allow overriding
  {
    User user = null;
    try
    {
      user = GetByID_EXEC (userID);
    }
    catch (UserNotFoundException)
    {
      throw;  // rethrow exception because it's the correct type.
    }
    catch (Exception)
    {
      // do whatever you want
    }
    if (user != null)
      return user;
    throw new UserNotFoundException ();
  }

  protected abstract User GetByID_EXEC (int userID);
}
Tobias Knauss
fuente