Progettazione per un'implementazione dell'interfaccia che fornisce funzionalità aggiuntive

3

C'è un problema di progettazione che ho trovato mentre implementavo un'interfaccia:

Diciamo che esiste un'interfaccia Device che promette di fornire funzionalità PerformA() e GetB() . Questa interfaccia sarà implementata per più modelli di un dispositivo. Cosa succede se un modello ha una funzionalità aggiuntiva CheckC() che non ha equivalenti in altre implementazioni?

Ho trovato diverse soluzioni, nessuna delle quali sembra conforme alle linee guida sulla progettazione dell'interfaccia:

  • Per aggiungere il metodo CheckC() all'interfaccia e lasciare uno dei suoi implementazioni vuote:
interface ISomeDevice
{
    void PerformA();
    int GetB();
    bool CheckC();
}

class DeviceModel1 : ISomeDevice
{
    public void PerformA() { // do stuff }
    public int GetB() { return 1; }
    public bool CheckC() {
        bool res;
        // assign res a value based on some validation
        return res;
    }
}

class DeviceModel2 : ISomeDevice
{
    public void PerformA() { // do stuff }
    public int GetB() { return 1; }
    public bool CheckC() {
        return true; // without checking anything
    }
}

Questa soluzione sembra errata in quanto una classe implementa un'interfaccia senza veramente implementando tutti i metodi richiesti.

  • Per omettere il metodo CheckC() dall'interfaccia e utilizzare il cast esplicito per chiamarlo:
interface ISomeDevice
{
    void PerformA();
    int GetB();
}

class DeviceModel1 : ISomeDevice
{
    public void PerformA() { // do stuff }
    public int GetB() { return 1; }
    public bool CheckC() {
        bool res;
        // assign res a value based on some validation
        return res;
    }
}

class DeviceModel2 : ISomeDevice
{
    public void PerformA() { // do stuff }
    public int GetB() { return 1; }
}

class DeviceManager
{
    private ISomeDevice myDevice;
    public void ManageDevice(bool newDeviceModel)
    {
        myDevice = (newDeviceModel) ? new DeviceModel1() : new DeviceModel2();
        myDevice.PerformA();
        int b = myDevice.GetB();
        if (newDeviceModel)
        {
            DeviceModel1 newDevice = myDevice as DeviceModel1;
            bool c = newDevice.CheckC();
        }
    }
}

Questa soluzione sembra rendere l'interfaccia incoerente.

  • Per il dispositivo che supporta CheckC() : per aggiungere la logica di CheckC() nella logica di un altro metodo presente nell'interfaccia. Questa soluzione non è sempre possibile.

Quindi, qual è il design corretto da utilizzare in questi casi? Forse la creazione di un'interfaccia dovrebbe essere abbandonata del tutto a favore di un altro design?

    
posta Limbo Exile 04.06.2014 - 11:38
fonte

2 risposte

2

Considera questa soluzione:

interface IDevice
{
    void PerformA();
    int GetB();
}

interface INewDevice : IDevice
{
    bool CheckC();
}

class DeviceModel1 : INewDevice
{
    ...
}


class DeviceModel2 : IDevice
{
    ...
}

class DeviceManager
{
    private IDevice myDevice;
    public void ManageDevice(bool newDeviceModel)
    {
        myDevice = (newDeviceModel) ? new DeviceModel1() : new DeviceModel2();
        myDevice.PerformA();
        int b = myDevice.GetB();
        if (newDeviceModel)
        {
            INewDevice newDevice = myDevice as INewDevice;
            bool c = newDevice.CheckC();
        }
    }
}

Ovunque non ti preoccupi della differenza tra un IDevice e un dispositivo INew, puoi utilizzare i metodi comuni a entrambi. Se hai bisogno di un comportamento specifico per i nuovi dispositivi, esegui il cast sulla nuova interfaccia, quindi non ti legare a nessuna particolare implementazione.

Se il casting ti infastidisce, vedi questa domanda per i modi di creare un nuovo tipo che può contenere un valore di tipo A o B (o C, o ...) e fornisce un modo sicuro per agire in base al tipo qual è il tipo di valore in realtà. Mi piace particolarmente la risposta di Joey per l'utilizzo dei parametri e lambda nominati. Ad esempio, puoi creare raccolte che contengono Either<IDevice, INewDevice> e quindi puoi utilizzare il metodo Match per evitare qualsiasi casting. Per es.

IList<Either<IDevice, INewDevice>> devices = // get a list of devices
foreach (var device in devices) {
    device.Match(
        Left: oldDevice => // things to do if it's an old device
        Right: newDevice => // things to do if it's a new device
    );
}

Non c'è il rischio di fare un cast sbagliato usando questo approccio.

    
risposta data 04.06.2014 - 15:49
fonte
1

Dipende molto, davvero (ora è una sorpresa). Ecco alcuni aspetti da considerare:

Qual è il contratto dell'interfaccia?
Se l'interfaccia deve consentire performA e getB , ma device2 richiede checkC per essere chiamato, quindi non aderisce all'interfaccia. Se l'interfaccia definisce che C deve essere controllabile, allora device1 non aderisce. Se il tuo algoritmo usa quel controllo, potrebbe essere quest'ultimo.

Puoi definire un comportamento predefinito ragionevole?
Se è solo un assegno, puoi sempre restituire true.

È un'operazione facoltativa?
Definirlo come facoltativo. L'interfaccia potrebbe specificare che è consentita l'emissione di un'eccezione. Aggiungi un altro metodo CanCheckC che restituisce se l'operazione è supportata. Oppure aggiungi TryCheckC che restituisce true se è stato eseguito un controllo e restituisce il risultato in un parametro esterno.

È un nuovo tipo di interfaccia?
Se, in generale, i dispositivi non riescono a controllare C, ma qualche dispositivo di classe può, forse questa è una nuova interfaccia. Quindi aggiungi una nuova interfaccia IDeviceWithCCheck che fornisca le operazioni aggiuntive e utilizzi i controlli del tipo nell'algoritmo prima di trasmettere.

    
risposta data 04.06.2014 - 12:44
fonte

Leggi altre domande sui tag