Devo usare l'ereditarietà per differenziare gli oggetti anche se hanno gli stessi campi?

2

Considera questa semplice classe che modella un dispositivo mobile reale:

/// <summary>
///     Model that represents a device.
/// </summary>
public class Device
{
    public DateTime CreationDate { get; set; }

    public bool Enabled { get; set; }

    /// <summary>
    ///     Gets or sets the device's hardware identifier.
    /// </summary>
    /// <remarks>Commonly filled with data like device's IMEI.</remarks>
    public string HardwareId { get; set; }

    public int Id { get; set; }

    public DateTime? LastCommunication { get; set; }
}

Ora supponiamo che sia arrivato un nuovo requisito e ho bisogno di iniziare a differenziare un dispositivo per piattaforma, come Windows Phone , Android e iOS . L'opzione più comune credo sarebbe quella di creare una classe di enumerazione:

/// <summary>
///     Defines a type of device.
/// </summary>
enum PlatformType
{
    /// <summary>
    ///     Device is an Android.
    /// </summary>
    Android,

    /// <summary>
    ///     Device is an Windows platform.
    /// </summary>
    Windows,

    /// <summary>
    ///     Device is an iOS platform.
    /// </summary>
    Ios,
}

e quindi aggiungi una nuova proprietà alla classe del dispositivo:

public class Device
{
    ...

    public PlatformType Platform { get; set; }

    ...
}

Un altro approccio sarebbe quello di creare una gerarchia di ereditarietà sulla classe del dispositivo stesso:

public class AndroidDevice : Device { }

public class IosDevice : Device { }

public class WindowsPhoneDevice : Device { }

Quando dovrei scegliere uno di questi approcci rispetto all'altro? Dovrei sempre scegliere il secondo o c'è un motivo per non farlo?

    
posta julealgon 05.05.2015 - 21:40
fonte

5 risposte

7

In generale preferisco composition a inheritance . Questo ha diversi motivi:

  • Gli esseri umani sono cattivi nell'affrontare la complessità. E occuparsi di alberi ad alto patrimonio è la complessità. Voglio strutture leggere sul mio cervello, che potrei trascurare facilmente. Lo stesso vale per dozzine di tipi anche derivati da una classe base.

  • Sei in grado di scambiare parti in movimento . Potresti avere un dispositivo semplice, che fa runOperatingSystem() , invece di creare due diversi% di% di memoria, fai solo uno e dagli un device . Se vuoi testare il comportamento, puoi iniettare un Operating System e vedere, se lo fa, che cosa dovrebbe.

  • Sei in grado di estendere il comportamento, semplicemente iniettando più componenti comportamentali.

In termini di mockOperatingSystem , stai meglio, progettando un tipo di dispositivo generico : In Python il design sarebbe simile al seguente

class Device:
    def __init__(self, OS, name):
        self.OS=OS
        self.name=name
    def browseInterNet(self):
        self.OS.browseInterNet()

class Android:
    def browseInterNet(self):
        print("I'm browsing")

Hai un generico abstraction che esegue un device . Ogni interazione utente è delegata a questo operating system . Forse vuoi testare solo la chiamata navigazione che dipende da qualsiasi sistema operativo, puoi facilmente sostituirlo.

Il prossimo passo per questo disegno sarebbe, per creare un OS -object, che prende i parametri comuni ( configuration , CreationDate e così via). Inietta questo HardwareId e il configuration appropriato tramite l'iniezione del costruttore e hai finito.

Definisci comportamento comune in un contratto ( OS ), che determina cosa potresti fare con un telefono e il sistema operativo si occupa dell'implementazione .

Tradotto nella lingua di tutti i giorni: se scrivi messaggi con il tuo telefono, non c'è differenza nel farlo con un iPhone, Android o Windows a tale riguardo, che tu sei texting , anche se i meccanismi del SO a OS differiscono. Il modo in cui si comportano tecnicamente non è interessante per il dispositivo. Il sistema operativo esegue interface , che a sua volta si occupa della sua implementazione. È tutta una questione di elementi comuni di astrazione .

D'altra parte: questo è solo il modo uno di farlo. Dipende da te e dal tuo modello del dominio.

Dai commenti :

But you have to agree with me that this would require one to create a lot of wrapper methods to make the API simpler

Dipende da ciò che esattamente vuoi modellare. Per estendere l'esempio dato di texting app :

Dire, hai semplicemente alcuni lavori di base, vuoi che il dispositivo faccia, definisci un texting per quello; nel nostro caso, semplicemente il metodo API . Hai quindi sendSMS(text) , in cui viene richiamato il messaggio Device . Il "send text" a sua volta delega quella chiamata al sistema operativo utilizzato, che esegue l'invio effettivo.

Se vuoi modellare più di una manciata di device delle offerte services , devi fare un eccesso di device .

Questo è un segno che il tuo livello di astrazione è troppo basso.

Il prossimo passo sarebbe quello di astrarre il concetto di un'app dal sistema.

In linea di principio, hai un API che interagisce con la logica interna di device , che esegue su un app . Hai operating system , che è elaborato e modifica il input del dispositivo.

In termini di MVC , ad es. la tua tastiera è display , il display è controller e c'è l' view all'interno dell'app, che viene modificata. E poiché la vista osserva il modello riflette eventuali cambiamenti.

Con un concetto così astratto, sei molto flessibile per costruire / modellare molti casi d'uso.

I also am not seeing exactly how you would handle the operations concept here. There are still many types of operations, each with differing parameters, that can or can't be applied to a given device.

Come detto sopra: è tutta una questione di astrazione. Più potenza vuoi, più il tuo modello deve essere astratto.

Where would the OS be located now after this abstraction?

Prendendo ulteriormente l'esempio, devi sviluppare diverse astrazioni / schemi, che aiutano in questo caso.

  • model -Pattern: il sistema operativo funge da mediatore, cioè prende segnali in forma di Mediator , lo invia all'app e prende in risposta commands ad es. aggiorna la vista

  • commands -Pattern: il pattern di comando è la forma dell'astrazione, che viene utilizzata per descrivere il flusso di comunicazione tra i componenti. Supponiamo che l'utente preme Command , che potrebbe essere astratto come A -comando con un valore di Keypressed . Un altro comando sarebbe A con il valore di update display o in questo caso keypress.value .

  • A Il display come MVC , la tastiera come view e tra il modello (app-).

La tua immaginazione è il tuo limite.

Questo è il tipo di materiale, OOP è stato originariamente inventato: simulazione di componenti indipendenti e loro interazione

    
risposta data 08.05.2015 - 20:05
fonte
4

Vorrei fare questa scelta in base a quanto dell'implementazione è condivisa tra le piattaforme.

Se il 99% del codice è uguale su tutte le piattaforme, allora disponi di un enum privato e in uno o due punti in cui è importante utilizzare solo un blocco switch(Platform) e il gioco è fatto.

Ma se devi fare dozzine di chiamate di sistema dipendenti dalla piattaforma all'interno di questa classe, ognuna con diversi parametri e semantica e convenzioni per la gestione degli errori, allora stai meglio con classi completamente separate che ereditano semplicemente da un'interfaccia comune.

L'interfaccia di esempio che hai mostrato verrebbe quasi sicuramente nella seconda categoria.

    
risposta data 05.05.2015 - 21:59
fonte
1

La risposta OOP è fare l'eredità. Immagino che il ragionamento sia di evitare blocchi condizionali di codice come:

if(type==PlatformType.Android) { ..}
else ....

Che può essere sostituito con metodi sovrascritti sulla classe specifica

Tuttavia, se si dispone solo di una struttura dati senza logica, come potrebbe essere il caso se non si ha necessità di campi aggiuntivi o se si passa a un DB o un servizio. Penso che la proprietà del tipo sia ok.

    
risposta data 05.05.2015 - 22:08
fonte
1

Se c'è un sacco di "codice comune" (o astrazioni che fanno uso di tutte le piattaforme), un'altra opzione è la composizione, piuttosto che l'ereditarietà. Credo che qualsiasi cosa tu possa fare con l'ereditarietà, puoi anche realizzare attraverso la composizione (probabilmente mi pentirò di dire "qualsiasi cosa"). L'ereditarietà ti bloccherà in quell'implementazione di base, la composizione ti dà una scelta.

Usando un esempio forzato, se stavi per implementare una classe base con funzionalità comuni come:

class BaseFoo
{
    public DoThis() {
    }

    public DoThat() {
    }
}

E sottoclassi:

class MyFoo : BaseFoo {
}

class YourFoo : BaseFoo {
}

Quelle sottoclassi sono bloccate nel comportamento della classe base. (Sì, puoi rendere i metodi virtuali e sovrascrivere il loro comportamento, ma, per amor di discussione, diciamo che ciò che i metodi di base fanno è estremamente complesso e non vuoi / non puoi ri-implementarlo.)

Se invece avevi implementato la stessa interfaccia (che rappresentava l'astrazione Foo ) e componevi le classi, sono liberi di implementare l'astrazione come meglio ritengono:

interface IFoo
{
    void DoThis();
    void DoThat();
}

class MyFoo : IFoo {
    private readonly IThisStrategy _thisStrategy;
    private readonly IThatStrategy _thatStrategy;

    public MyFoo(IThisStrategy thisStrategy, IThatStrategy thatStrategy) {
        _thisStrategy = thisStrategy;
        _thatStrategy = thatStrategy;
    }

    public void DoThis() {
        _thisStrategy();
    }
}

Potrebbe essere necessario un po 'più di lavoro per implementare la soluzione attraverso la composizione, ma i vantaggi includono disaccoppiamento, flessibilità e componenti riutilizzabili (le implementazioni della strategia). Questo ha anche dei vantaggi in termini di test in quanto è possibile testare i singoli comportamenti che si stanno iniettando senza dover testare l'oggetto in generale. E questo evita anche di inquinare il tuo codice con una serie di istruzioni switch ovunque che in seguito causano un mal di testa di manutenzione nel caso avessi bisogno di supportare una nuova piattaforma o cambiare il comportamento di quella piattaforma.

Credo fermamente nella composizione sull'ereditarietà in moltissimi casi, specialmente quando l'ereditarietà non viene utilizzata per estendere effettivamente una base funzionale.

    
risposta data 08.05.2015 - 19:06
fonte
0

In realtà nessuno di noi può dirti il modo migliore per implementare il tuo tipo di dispositivo, dal momento che non sappiamo come (ri) utilizzare il tuo codice. Il problema è che alcuni sviluppatori dicono che dovresti usare Device come interfaccia, AbstractDeviceImpl come classe astratta e Android , ecc. Come una classe concreta che sarebbe carina, ma ne hai bisogno?

Direi che l'approccio migliore è scoprirlo da soli usando TDD. Usa il tuo codice, guarda cosa succede. Cerca di scoprire i tuoi requisiti, leggendo i tuoi test. Vuoi che sia preparato per l'OCP e molto astratto? O non ci saranno altri dispositivi così semplicemente un'astrazione del dispositivo e 3 classi concrete sono i tuoi amici?

Poiché penso che non ci saranno più di quei 3 dispositivi, e supponiamo che tu abbia solo bisogno del tipo per logging / debugging, creerei una classe Device implementando type come stringa di sola lettura unknown come predefinito. Quindi crea 3 classi interne che ereditano Device sovrascrivendo type -getter con il nome corrispondente.

Di fatto, l'utilizzo di un enum in questo modo è stato descritto in precedenza, è in gran parte una violazione OCP grave che termina in blocchi di switch e / o in istruzioni. Aggiungere un nuovo dispositivo significa cambiare l'enumerazione e aggiungere una classe. Questo in realtà non è ciò che un OOP vuole. Puoi sempre usare le enumerazioni se hai bisogno del valore stesso, ma per distinguere tra i percorsi di esecuzione, enum con più di due elementi potrebbe produrre un codice brutto.

    
risposta data 09.05.2015 - 01:18
fonte

Leggi altre domande sui tag