Ereditarietà / domanda di progettazione OO

4

Probabilmente si tratta di una domanda abbastanza semplice, ma per prima cosa ha bisogno di un po 'di background ... Sto modellando vari switch hardware, che possono essere attivati e disattivati. Gli switch sono configurabili, consentendo a un utente di specificare i valori numerici che vengono inviati all'hardware per segnalare che dovrebbe accendersi o spegnersi. Questa è la classe Switch : -

public class Switch
{
    public Id { get; set; }
    public SwitchConfig Config { get; set; }

    public void TurnOn()
    {
        WriteToSerialPort(Id, Config.OnValue);
    } 

    public void TurnOff()
    {
        WriteToSerialPort(Id, Config.OffValue);
    } 
}

public class SwitchConfig
{
    public int OnValue { get; set; }
    public int OffValue { get; set; }
}

Fin qui tutto bene.

Ora devo modellare un nuovo tipo di interruttore che non solo può essere attivato e disattivato, ma può anche avere una soglia di temperatura impostata, quindi sembra ragionevole ereditare dalla classe Switch . La configurazione dovrà anche estendersi per includere temperature min / max consentite, ad esempio: -

public TemperatureSwitchConfig : SwitchConfig
{
    public int MinTemperature { get; set; }
    public int MaxTemperature { get; set; }
}

La classe switch specializzata sarà simile a questa: -

public TemperatureSwitch : Switch
{
    public void SetTemperature(int temperature)
    {
        var temperatureSwitchConfig = Config as TemperatureSwitchConfig;

        if (temperature < temperatureSwitchConfig.MinTemperature
            || temperature > temperatureSwitchConfig.MaxTemperature)
        {
             return;
        }

        WriteToSerialPort(Id, temperature);
    }
}

Non sembra "giusto" dove devo castare la proprietà Config . Sono certo che a questo deve esserci una soluzione OO di punta, ma penso che sia intervenuta la "paralisi dell'analisi" e non riesco a vedere il legno per gli alberi! O mi preoccupo per nulla - il cast è una soluzione accettabile in questo scenario?

    
posta Andrew Stephens 25.04.2014 - 13:49
fonte

6 risposte

1

Ti consiglio di rimuovere la proprietà Config dalla classe e di inserirne tutte le proprietà nella classe stessa. Quindi, imposti tali proprietà nel costruttore di ogni switch o come proprietà.

Dici che SwitchConfig proviene dalla serializzazione XML. Che cosa succede se si desidera parametrizzare tale serializzazione in qualche modo, di solito viene eseguita utilizzando gli attributi nelle classi del modello ( SwitchConfig nel tuo caso). Quindi SwitchConfing e transitivamente Switch diventa dipendente dal modulo per la serializzazione XML.

Che cosa succede se si desidera creare gli switch in base a qualcosa di diverso da XML? Quindi dovrai creare inutilmente le classi Config , invece di impostare i parametri in costruttori o proprietà.

L'ultima cosa che viene in mente è: come crei un tipo concreto di Switch di classe? Per esempio. come decidi di creare Switch o TemperatureSwitch ? Probabilmente basato sul tipo di SwitchConfig . Se hai già un codice come questo, perché non trasformarlo in uno stabilimento completo, che costruisca switch specifici basati su dati XML? E la serializzazione XML può essere incapsulata internamente all'interno di questo stabilimento.

E se ti preoccupi di dover cambiare molte cose quando la configurazione cambia, allora puoi fare quello che ha detto Karl Bielefeldt e usare la deserializzazione con caratteri deboli invece del modello di classe esplicito.

    
risposta data 25.04.2014 - 15:52
fonte
4

Il tuo commento sulla necessità di generare Config da xml aggiunge una nuova dimensione al problema. I tipi forti sono più utili in fase di compilazione, che non è applicabile qui. La mia inclinazione sarebbe quella di creare invece un Dictionary per contenere la configurazione analizzata, e usarla come:

public TemperatureSwitch : Switch
{
    public void SetTemperature(int temperature)
    {
        if (temperature < config["MinTemperature"]
         || temperature > config["MaxTemperature"])
        {
             return;
        }

        WriteToSerialPort(Id, temperature);
    }
}

Puoi inserire la convalida nel tuo codice parser o nel codice setter. In alternativa, puoi semplicemente rinunciare del Config del tutto e avere il codice del parser xml per creare direttamente gli oggetti Switch , con i singoli campi per gli elementi di configurazione.

    
risposta data 25.04.2014 - 14:54
fonte
1

Puoi configurare il parametro config:

public abstract class Switch<TConfig> where TConfig : SwitchConfig
{
    public Id { get; set; }
    public TConfig { get; set; }

    public void TurnOn()
    {
        WriteToSerialPort(Id, Config.OnValue);
    } 

    public void TurnOff()
    {
        WriteToSerialPort(Id, Config.OffValue);
    } 
}

public class NormalSwitch : Switch<SwitchConfig>
{
}

public TemperatureSwitch : Switch<TemperatureSwitchConfig>
{
    public void SetTemperature(int temperature)
    {

        if (temperature < Config.MinTemperature
            || temperature > Config.MaxTemperature)
        {
             return;
        }

        WriteToSerialPort(Id, temperature);
    }
}
    
risposta data 25.04.2014 - 14:50
fonte
0

Bene, puoi usare i generici C # (o equivalenti nella tua scelta della lingua). quindi in pratica il tuo primo blocco di codice sarà simile a:

    public class Switch<T> where T:SwhitchConfig
    {
       public Id { get; set; }
       public T Config { get; set; }

       public void TurnOn()
       {
           WriteToSerialPort(Id, Config.OnValue);
       } 

       public void TurnOff()
       {
            WriteToSerialPort(Id, Config.OffValue);
       } 
   }

   public class SwitchConfig
   {
       public int OnValue { get; set; }
       public int OffValue { get; set; }
   }

E la tua implementazione specializzata sarà simile a:

    public TemperatureSwitch : Switch<TemperatureSwitchConfig>
    {

    }        
    
risposta data 25.04.2014 - 14:54
fonte
0

La maggior parte dei linguaggi OO implementa correttamente le regole di sottotipizzazione, come affermato da Barbara Liskov (in "Una nozione comportamentale di sottotipizzazione" , vedi figura 4). In particolare, se hai un metodo con la firma

X method(Y arg1, Z arg2);

quindi puoi sovrascrivere questo metodo in una sottoclasse usando la firma

A method(B arg1, C arg2);

dove:

  • A è una sottoclasse di X (o è X)
  • B è una superclasse di Y (o è Y)
  • C è una superclasse di Z (o è Z)

Nel tuo caso, la tua classe TemperatureSwitch potrebbe sovrascrivere

SwitchConfig Switch::Config();

con

TemperatureSwitchConfig Switch::Config();

Quindi, se hai una variabile di tipo TemperatureSwitch , il suo metodo Config ti restituirà un TemperatureSwitchConfig . Se hai una variabile di tipo Switch che punta effettivamente a TemperatureSwitch , la sua Config method ti restituirà un SwitchConfig , che in realtà è un TemperatureSwitchConfig (proprio come la tua situazione attuale).

    
risposta data 25.04.2014 - 14:47
fonte
-1

Disclaimer : My C # è piuttosto arrugginito. La sintassi potrebbe essere disattivata.

I now need to model a new type of switch that can not only be turned on and off, but can also have a temperature threshold set, so it seems sensible to inherit from the Switch class.

Una risposta naturale, ma dovresti stare attento. L'ereditarietà è pericolosa , non dovrebbe essere la tua prima reazione a un problema.

Ecco la mia opinione su questo:

Innanzitutto, poiché tutti i tuoi campi sono pubblici, non ha senso creare metodi TurnOn e TurnOff . Sarebbe leggermente meglio avere queste funzioni statiche. Il lato positivo è che quando è necessario modificare l'implementazione di TurnOn / TurnOff o fornire implementazioni alternative, non è necessario ricompilare tutto ciò che dipende da Switch .

Successivamente, puoi facilmente rendere Switch immutable e dovresti preferire l'immutabilità quando possibile. Rendendolo immutabile non devi preoccuparti di tenere traccia delle possibili modifiche di stato mentre passi valori di Switch nel tuo programma. Ti permette anche di usarli come chiavi nei dizionari, poiché l'uguaglianza e l'hashcode di una chiave non dovrebbero dipendere dallo stato mutabile.

Con tutto ciò, dobbiamo affrontare il problema che c'è più di un tipo di interruttore. È necessario prendere una decisione qui: voglio introdurre facilmente nuovi tipi di switch o semplificare le operazioni con gli switch che già possiedi? Dato che si tratta di switch hardware, supponiamo che l'aggiunta di nuovi tipi di switch sarà un evento relativamente raro. In tal caso, trarrai vantaggio dall'utilizzo di un tipo di somma . Questo ci limiterà a un insieme finito di tipi di switch, ma ci fornirà un modo sicuro per distinguerli e assicurarci di gestirli appropriatamente. Quindi, il tuo codice andrebbe in questo modo (costruttori omessi per brevità):

// Switch.cs
public abstract Switch {
    private Switch() {} // Prevent inheritance outside of this scope

    public abstract R Match<R>(Func<NormalSwitch, R> IfNormal, Func<TemperatureSwitch, R> IfTemp);

    public sealed NormalSwitch : Switch {
        public <IdType> Id { get; }
        public SwitchConfig Config { get; }

        public R Match<R>(Func<NormalSwitch, R> IfNormal, Func<TemperatureSwitch, R> IfTemp); {
            return IfNormal(this);
        }
    }

    public sealed TemperatureSwitch : Switch {
        public <IdType> Id { get; }
        public TempSwitchConfig Config { get; }

        public R Match<R>(Func<NormalSwitch, R> IfNormal, Func<TemperatureSwitch, R> IfTemp); {
            return IfTemp(this);
        }
    }
}

// SwitchFunctions.cs
public static class SwitchFunctions {
    public static void TurnOn(NormalSwitch Switch) { ... }
    public static void TurnOff(NormalSwitch Switch) { ... }
}

// TempSwitchFunctions.cs
public static class TempSwitchFunctions {
    public static void TurnOn(TemperatureSwitch Switch) { ... }
    public static void TurnOff(TemperatureSwitch Switch) { ... }
    public static void SetTemperature(TemperatureSwitch Switch) { ... }
}

Ora nel tuo codice cliente puoi fare questo:

Switch switch = parseXml()
switch.match(
    IfNormal: NormalSwitch => TurnOn(NormalSwitch),
    IfTemperature: TempSwitch => {
        TurnOn(TempSwitch);
        SetTemperature(TempSwitch);
    }
);

È tutto strongmente digitato, e puoi scrivere implementazioni alternative alle funzioni dello switch senza dover ricompilare tutto ciò che dipende dalle implementazioni originali.

Uh oh, ho introdotto alcune programmazioni funzionali nel tuo codice. Ma va bene, dato che Switch era solo un contenitore di dati.

    
risposta data 25.04.2014 - 15:07
fonte

Leggi altre domande sui tag