Como impor a implementação da interface para se comportar de uma certa maneira

8

Suponha que você tenha a seguinte interface

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

Como você aplicaria aos implementadores dessa interface uma exceção se um usuário não fosse encontrado?

Eu suspeito que não é possível fazer apenas com o código; então, como você aplicaria os implementadores a implementar o comportamento pretendido? Seja através de código, documentação, etc.?

Neste exemplo, espera-se que a implementação concreta lance um 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");
        }
    }
}
Mateus
fonte
3
Esta é realmente uma pergunta muito interessante. Sinto que a resposta correta pode envolver Atributos ou um hack de IL. Geralmente, eu diria que tentar fazer uma escolha errada, porque cada implementador deve ser capaz de lançar seus próprios tipos de exceções, mas ainda é concebivelmente útil. Talvez você use apenas uma classe abstrata com um método de modelo?
Magus
15
A tecnologia não resolve os problemas das pessoas. Se as pessoas violarem a interface, bata nelas com um pau até parar.
Telastyn
1
@Telastyn Talvez eu deva criar um programa que derrote as pessoas com paus.
Matthew
2
@Doval: Então, como a Interface sabe se um usuário válido existe ou não sem uma implementação que o apóie?
Robert Harvey
1
@matthew - então seu programa teria que inferir "até que parem" corretamente, o que leva a toda a questão do problema de parada (se o seu programa de manuseio de bastões pudesse dizer que as pessoas estavam violando sua interface ou não, você não precisaria disso) .
Telastyn

Respostas:

9

Esse é um recurso de idioma que foi intencionalmente omitido do C # . Simplesmente, é perfeitamente possível que uma IUserRepository.GetByIDfalha por algum outro motivo não seja o usuário que não foi encontrado, portanto você não deseja exigir um erro específico quando isso não pode acontecer. Você tem duas opções se, por qualquer motivo, quiser impor esse comportamento:

  1. Defina a Userclasse para que ela mesma gere a exceção se for inadequadamente inicializada.
  2. Escreva testes de unidade para IUserRepositoryesse teste explicitamente para esse comportamento.

Observe que nenhuma dessas opções é "inclua-a na documentação". Idealmente, você deve fazer isso de qualquer maneira, especialmente porque é na documentação que você pode explicar por que deseja aplicar um tipo de erro específico.

DougM
fonte
2
Mesmo com exceções verificadas, você não pode forçar os implementadores a lançar a exceção certa no momento certo.
Svick
7

Não há como exigir que uma implementação lance uma exceção por meio de uma interface, mesmo em linguagens como Java, onde você pode declarar que um método pode lançar uma exceção.

Pode haver uma maneira de garantir (até certo ponto, mas não absolutamente) que uma exceção seja lançada. Você pode criar uma implementação abstrata da sua interface. Em seguida, você pode implementar o GetUsermétodo como final na classe abstrata e usar o padrão de estratégia para chamar outro membro protegido da subclasse e lançar uma exceção se retornar algo diferente de um usuário válido (como nulo). Isso ainda pode cair se, por exemplo, o outro desenvolvedor retornar um tipo de objeto nulo User, mas eles realmente precisariam trabalhar para subverter a intenção aqui. Eles também podem apenas reimplementar sua interface, também são ruins, portanto, você pode considerar substituir a interface inteiramente pela classe abstrata.

(Resultados semelhantes podem ser alcançados usando delegação em vez de subclassificar com algo como um decorador de embalagens.)

Outra opção poderia ser criar um conjunto de testes de conformidade que todo código de implementação deve passar para ser incluído. A eficácia disso depende de quanto controle você tem sobre a vinculação do outro código ao seu.

Também concordo com outras pessoas que a documentação e a comunicação claras são fornecidas quando um requisito como esse é esperado, mas não pode ser completamente imposto no código.


Exemplos de código:

Método da subclasse:

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
}

Um último ponto rápido que quero destacar que esqueci antes é que, ao fazer qualquer uma dessas opções, você está se comprometendo a uma implementação mais específica, o que significa que os desenvolvedores da implementação têm muito menos liberdade.

cbojar
fonte
3

Se você trabalhar em conjunto com os desenvolvedores que deseja implementar o comportamento de lançamento de exceção, poderá escrever unittests (para eles) para verificar se uma exceção é lançada se tentar obter um usuário que não existe.

Portanto, você precisa saber quais métodos testar. Eu não estou familiarizado com C #, mas talvez você possa usar a reflexão para procurar todas as classes que implementam o IUserRepository e testá-las automaticamente, mesmo que um desenvolvedor adicione outra classe, ela será testada quando os testes forem executados.

Mas isso é apenas o que você poderia fazer na prática se realmente sofrer com desenvolvedores fazendo as implementações erradas com as quais precisa trabalhar. Em teoria, as interfaces não são para definir como a lógica de negócios deve ser implementada.

Estou ansioso por outras respostas, pois essa é realmente uma pergunta interessante!

valenterry
fonte
3

Você não pode. Essa é a questão das interfaces - elas permitem que qualquer pessoa as implemente a qualquer momento. Há um número infinito de possíveis implementações para sua interface e você não pode forçar nenhuma delas a estar correta. Ao escolher uma interface, você perdeu o direito de impor que uma implementação específica seja usada em todo o sistema. Caramba, o que você tem nem é uma interface, é uma função. Você está escrevendo um código que transmite funções.

Se você precisar seguir esse caminho, basta documentar claramente a especificação com a qual as implementações devem estar em conformidade e confiar que ninguém quebrará deliberadamente essa especificação.

A alternativa é ter um tipo de dados abstrato, que se traduz em uma classe em linguagens semelhantes a Java / C #. O problema é que os idiomas principais têm suporte fraco para ADTs e você não pode alternar a implementação da classe sem o tomfoolery do sistema de arquivos. Mas se você deseja viver com isso, pode garantir que haja apenas uma implementação no sistema. Isso é um avanço das interfaces, nas quais seu código pode quebrar a qualquer momento e, quando isso acontecer, você terá que descobrir qual implementação da interface quebrou seu código e de onde veio.

EDIT : Observe que mesmo os hacks de IL não permitirão garantir a correção de certos aspectos de uma implementação. Por exemplo, é assumido implicitamente que GetByIDdeve retornar um valor ou lançar uma exceção. Alguém poderia escrever uma implementação que simplesmente entra em um loop sem fim e não o faz. Você não pode provar durante o tempo de execução que o código é repetido para sempre - se você conseguir fazer isso, resolveu o problema da parada. Você pode detectar casos triviais (por exemplo, reconhecer a while (true) {}), mas não um código arbitrário.

Doval
fonte
Bem, uma interface evita algumas implementações erradas: aquelas com erros de digitação (não coincidem com as assinaturas de método na interface). Ele não pode expressar a restrição que o OP deseja, mas poucos sistemas de tipos conseguem isso.
@ delnan Se um pedaço de código tem um tipo diferente, você dificilmente pode chamá-lo de implementação! Mas estamos discutindo semântica. Eu entendi o que você quis dizer.
Doval
Outra coisa: Um "tipo de dado abstrato" é uma noção informal, semelhante à documentação; para torná-lo verificável pela máquina, você precisa construir um sistema de tipos e usar o equivalente às interfaces desse sistema de tipos.
@ delnan Isso é um equívoco. Consulte Noções básicas sobre abstração de dados, revisitado . Um ADT abstrai dados como uma interface, sim. Mas um ADT faz isso obscurecendo a identidade de um tipo existente. Isso é equivalente a uma classe com campos particulares no OOP convencional. O tipo obscuro é o registro cujos campos você tornou privados. Uma interface, por outro lado, é um pacote de funções e o tipo de implementação não aparece em nenhum lugar nas assinaturas. É isso que permite múltiplas implementações e substituibilidade.
Doval
Este parece ser um uso bastante diferente do "tipo de dados abstratos" do que estou acostumado; o que eu e muitos outros conhecemos como tipo de dados abstratos é apenas uma ferramenta para falar sobre tipos de dados de uma maneira independente de idioma. O que você descreve parece ser um (s) (s) recurso (s) do sistema de tipos que interpretam e implementam a noção acima mencionada de tipos de dados abstratos.
2

Não há como 100% forçar o comportamento que você deseja, mas usando contratos de código, você poderia dizer que as implementações não devem retornar nulas, e talvez o objeto User tenha o ID passado (portanto, um objeto com o userid 0 falharia se 1 fosse passado ) Além disso, documentar o que você espera das implementações também pode ajudar.

Andy
fonte
0

Provavelmente é muito tarde para responder a essa pergunta, mas gostaria de compartilhar meu pensamento sobre isso. Como todas as respostas sugerem, não é possível forçar a restrição com a Interface. Mesmo com hacks seria difícil.

Uma maneira de conseguir isso, provavelmente específico para o .NET, é criar a interface como abaixo -

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

A tarefa não forçará a lançar certo tipo de exceção, mas tem disposição para transmitir a intenção de todo o implementador dessa interface. As tarefas no .NET têm certo padrão de uso, como Task.Result para acessar o resultado, Task.Exception, se houver exceção. Além disso, isso também permitirá ao usuário usá-lo de forma assíncrona.

Bhalchandra K
fonte
0

Você não pode fazer isso com uma interface, como muitas outras já responderam.
Mas você pode fazer isso com uma classe base abstrata:

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
fonte