Come imporre l'implementazione dell'interfaccia per comportarsi in un certo modo

8

Supponiamo di avere la seguente interfaccia

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

Come imporvi gli implementatori di questa interfaccia a generare un'eccezione se un utente non viene trovato?

Sospetto che non sia possibile fare con il solo codice, quindi come imporrebbe agli implementatori l'implementazione del comportamento previsto? Sia attraverso il codice, la documentazione, ecc.?

In questo esempio, è prevista l'implementazione concreta di 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");
        }
    }
}
    
posta Matthew 01.05.2014 - 23:06
fonte

6 risposte

9

Questa è una funzione linguistica che era omesso intenzionalmente da C # . Molto semplicemente, è del tutto possibile che un IUserRepository.GetByID abbia esito negativo per qualche altro motivo completamente diverso da quello in cui l'utente non viene trovato, quindi non vuoi richiedere un errore specifico quando ciò non può accadere. Hai due opzioni se per qualsiasi motivo vuoi applicare questo comportamento:

  1. Definisci la classe User in modo tale che essa stessa generi l'eccezione se è impropriamente initalizzata.
  2. Scrivi test unitari per IUserRepository che testano esplicitamente questo comportamento.

Si noti che nessuna di queste opzioni è "includerla nella documentazione". Idealmente, dovresti farlo comunque, soprattutto perché la documentazione è dove puoi spiegare perché vuoi imporre un particolare tipo di errore.

    
risposta data 01.05.2014 - 23:21
fonte
6

Non c'è modo di richiedere un'implementazione per generare un'eccezione attraverso un'interfaccia, anche in linguaggi come Java, dove puoi dichiarare che un metodo può generare un'eccezione.

Potrebbe esserci un modo per garantire (in una certa misura ma non assolutamente) che venga lanciata un'eccezione. Puoi creare un'implementazione astratta della tua interfaccia. È quindi possibile implementare il metodo GetUser come finale nella classe astratta e utilizzare il modello di strategia per chiamare un altro membro protetto della sottoclasse e generare un'eccezione se restituisce qualcosa di diverso da un utente valido (come null). Questo può ancora cadere se, per esempio, l'altro sviluppatore restituisce un tipo di oggetto nullo User , ma dovrebbero davvero lavorare per sovvertire l'intento qui. Potrebbero anche solo reimplementare la tua interfaccia, anche questa male, quindi potresti prendere in considerazione la possibilità di sostituire completamente l'interfaccia con la classe astratta.

(Risultati simili possono essere ottenuti utilizzando la delega anziché la sottoclasse con qualcosa come un decoratore di wrapping.)

Un'altra opzione potrebbe essere quella di creare una suite di test di conformità che tutti i codici di implementazione debbano passare per essere inclusi. Quanto è efficace dipende da quanto controllo hai sul collegamento dell'altro codice nel tuo.

Sono anche d'accordo con gli altri sul fatto che una chiara documentazione e comunicazione sono dati quando è richiesto un requisito come questo ma non può essere completamente applicato nel codice.

Esempi di codice:

Metodo sottoclasse:

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
}

Metodo decoratore:

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 ultimo punto rapido che voglio fare che ho dimenticato prima è che, facendo uno di questi, ti stai legando a un'implementazione più specifica, il che significa che gli sviluppatori di implementazione ottengono molta meno libertà.

    
risposta data 02.05.2014 - 00:05
fonte
3

Se collabori con gli sviluppatori per implementare il comportamento di lancio di eccezioni, potresti scrivere un univest (per loro) per verificare se viene lanciata un'eccezione se cerchi di ottenere un utente che non esiste.

Devi quindi sapere quali metodi testare. Non ho familiarità con C #, ma potresti forse usare la reflection per cercare tutte le classi che implementano il IUserRepository e testarle automaticamente così, anche se uno sviluppatore aggiunge un'altra classe, verrà testato quando verranno eseguiti i test.

Ma questo è solo ciò che potresti fare nella pratica se davvero soffri da parte degli sviluppatori che eseguono le implementazioni sbagliate con cui devi lavorare. In teoria le interfacce non servono a definire come devono essere implementate le logiche di business.

Non vedo l'ora di trovare altre risposte in quanto questa è davvero una domanda interessante!

    
risposta data 01.05.2014 - 23:25
fonte
3

Non puoi. Questo è il problema delle interfacce: consentono a chiunque di implementarle in qualsiasi momento. Esiste un numero infinito di potenziali implementazioni per l'interfaccia e non è possibile forzarne nessuna corretta. Scegliendo un'interfaccia, hai perso il diritto di far applicare una particolare implementazione a tutto il sistema. Diamine, quello che hai non è nemmeno un'interfaccia, è una funzione. Stai scrivendo il codice che passa le funzioni intorno.

Se devi seguire questa strada, devi semplicemente documentare chiaramente le specifiche a cui le implementazioni devono conformarsi e confidare che nessuno possa deliberatamente infrangere tale specifica.

L'alternativa è avere un tipo di dati astratto, che si traduce in una classe in linguaggi simili a Java / C #. Il problema è che le lingue mainstream hanno un debole supporto per le ADT e non è possibile cambiare l'implementazione della classe senza il buffonaggio del file system. Ma se hai intenzione di conviverci, puoi assicurarti che ci sia solo un'implementazione nel sistema. Questo è un passo avanti rispetto alle interfacce, in cui il tuo codice potrebbe interrompersi in qualsiasi momento, e quando lo farai dovrai capire quale implementazione dell'interfaccia ha rotto il tuo codice e da dove proviene.

EDIT : nota che anche gli hack di IL non ti permetteranno di garantire la correttezza di alcuni aspetti di un'implementazione. Ad esempio, è implicitamente presupposto che GetByID debba restituire un valore o generare un'eccezione. Qualcuno potrebbe scrivere un'implementazione che va semplicemente in un ciclo infinito e non fa nessuno dei due. Non puoi provare durante il runtime che il codice giri per sempre - se riesci a farlo, hai risolto il problema di interruzione. Potresti essere in grado di rilevare casi banali (ad esempio, riconoscere un while (true) {} ) ma non un pezzo di codice arbitrario.

    
risposta data 01.05.2014 - 23:22
fonte
2

Non è possibile forzare al 100% il comportamento desiderato, ma utilizzando i contratti di codice potresti diciamo che le implementazioni non devono restituire null, e forse che l'oggetto User ha passato l'id (quindi un oggetto con userid 0 fallirebbe se fosse passato 1). Inoltre, è utile documentare ciò che ci si aspetta dalle implementazioni.

    
risposta data 02.05.2014 - 03:12
fonte
0

Probabilmente è molto tardi per rispondere a questa domanda, ma vorrei condividere il mio pensiero su questo. Come suggerito da tutte le risposte, non è possibile forzare il vincolo con Interfaccia. Anche con gli hack sarebbe difficile.

Un modo per ottenere questo, probabilmente specifico per .NET è progettare l'interfaccia come sotto -

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

L'attività non imporrà di generare un certo tipo di Eccezione, ma prevede di trasmettere l'intenzione di tutti gli implementatori di questa interfaccia. Le attività in .NET hanno un determinato schema di utilizzo, ad esempio Task.Result per accedere al risultato, Task.Exception se c'è un'eccezione. Inoltre, questo userà anche l'uso in modo asincrono.

    
risposta data 23.11.2017 - 12:41
fonte

Leggi altre domande sui tag