2012-03-16 4 views
5

Ho eseguito in questo più volte, così vorrebbe usare un esempio reale e ottenere idee su come più esperti sviluppatori C# gestire questa situazione.progettazione di classe per il sistema, che è gerarchica, ma non così ordinatamente

Sto scrivendo un wrapper .NET attorno alla libreria non gestita MediaInfo, che raccoglie vari dati sui file multimediali (filmati, immagini ...).

MediaInfo ha molte funzioni, ogni applicazione a diversi tipi di file. Ad esempio, "PixelAspectRatio" si applica a immagini e video ma non a audio, sottotitoli o altri.

Un sottoinsieme delle funzionalità mi piacerebbe avvolgere è qui sotto:

General Video Audio Text Image Chapters Menu (Name of function)  
x  x  x  x  x  x   x  Format 
x  x  x  x  x  x   x  Title 
x  x  x  x  x  x   x  UniqueID 
x  x  x  x  x    x  CodecID 
x  x  x  x  x    x  CodecID/Hint 
     x  x  x  x    x  Language 
x  x  x  x  x      Encoded_Date 
x  x  x  x  x      Encoded_Library 
x  x  x  x  x      InternetMediaType 
x  x  x  x  x      StreamSize 
     x  x  x  x      BitDepth 
     x  x  x  x      Compression_Mode 
     x  x  x  x      Compression_Ratio 
x  x  x  x      x  Delay 
x  x  x  x      x  Duration 
     x  x  x        BitRate 
     x  x  x        BitRate_Mode 
     x  x  x        ChannelLayout 
     x  x  x        FrameCount 
     x  x  x        FrameRate 
     x  x  x        MuxingMode 
     x  x  x        MuxingMode 
     x  x  x        Source_Duration 
     x    x  x      Height 
     x    x  x      Width 
     x      x      PixelAspectRatio 
       x          SamplingRate 
x              Album 
x              AudioCount 
x              ChaptersCount 
x              EncodedBy 
x              Grouping 
x              ImageCount 
x              OverallBitRate 
x              OverallBitRate_Maximum 
x              OverallBitRate_Minimum 
x              OverallBitRate_Nominal 
x              TextCount 
x              VideoCount 

Come si può vedere, l'inizio di una mappatura di classe non troppo male sarebbe una classe per la funzionalità specifiche per ogni tipo di flusso e una classe base con funzionalità comuni a tutti i tipi.

Allora questo percorso diventa un po 'meno ovvio. Esistono molte funzioni comuni ai tipi di stream {general, video, audio, text e image}. Va bene, quindi credo che posso fare una classe con un nome puzzolente come "GeneralVideoAudioTextImage", e poi un altro di nome GeneralVideoAudioText (che eredita da GeneralVideoAudioTextImage) per la funzionalità comune a queste cose, ma non "Immagine" corsi d'acqua. Ciò, secondo me, seguirebbe maldestramente la regola "is a" delle gerarchie di classi.

questo è già senza guardare elegante, ma poi ci sono quei casi occasionali come "larghezza", che non rientrano in un gruppo che è in modo pulito un sottoinsieme di un altro gruppo. Questi casi possono semplicemente duplicare la funzionalità laddove necessario - implementarli singolarmente in Video, Testo e Immagine, ma ciò ovviamente violerebbe lo DRY.

Un primo approccio comune sarebbe MI, che C# non supporta. La solita risposta a questo sembra essere "usa MI con interfacce", ma non riesco a trovare del tutto in che modo possa seguire ASCIUTTO. Forse il mio fallimento.

gerarchie di classe sono stati discussi su SO prima, come hanno alternatives a MI (metodi di estensione, ecc), ma nessuna di queste soluzioni sembrava una buona forma. Ad esempio, i metodi di estensione sembrano meglio utilizzati per le classi di cui non è possibile modificare l'origine, come la classe String, e sono più difficili da individuare perché non sono realmente legate alla classe, sebbene possano funzionare. Non ho trovato una domanda su una situazione come questa, anche se questo è probabilmente un fallimento del mio utilizzo dello strumento di ricerca.

Un esempio di una caratteristica MediaInfo, avvolto, potrebbe essere:

int _width = int.MinValue; 
/// <summary>Width in pixels.</summary> 
public int width { 
    get { 
     if(_width == int.MinValue) 
      _width = miGetInt("Width"); 
     return _width; 
    } 
} 

// ... (Elsewhere, in another file) ... 
/// <summary>Returns a MediaInfo value as an int, 0 if error.</summary> 
/// <param name="parameter">The MediaInfo parameter.</param> 
public int miGetInt(string parameter) { 
    int parsedValue; 
    string miResult = mediaInfo.Get(streamKind, id, parameter); 
    int.TryParse(miResult, out parsedValue); 
    return parsedValue; 
} 

La mia domanda è: Come hai gestito situazioni come questa, sistemi che sono tipo-di-gerarchico-ma-non-proprio? Hai trovato una strategia abbastanza elegante, o hai semplicemente accettato che non tutti i semplici problemi ne hanno uno?

+0

Se non si trova una soluzione migliore, forse codegen utilizzando CodeDOM o Roslyn potrebbe aiutare. – svick

risposta

5

Penso che tu sia migliore fuori utilizzando una combinazione di interfacce e, se l'implementazione è più complessa di un mazzo di proprietà, la composizione per fornire implementazioni comuni delle interfacce:

abstract class Media { 
// General properties/functions 
} 

class VideoAndImageCommon { // Crappy name but you get the idea 
// Functions used by both video and images 
} 

interface IVideoAndImageCommon { 
// Common Video & Image interface 
} 

class Video : Media, IVideoAndImageCommon { 
    private readonly VideoAndImageCommon _commonImpl = new VideoAndImageCommon(); 

    // Implementation of IVideoAndImageCommon delegates to _commonImpl. 
} 

class Image : Media, IVideoAndImageCommon { 
    private readonly VideoAndImageCommon _commonImpl = new VideoAndImageCommon(); 

    // Implementation of IVideoAndImageCommon delegates to _commonImpl. 
} 
0

ho segnato Andrew Kennan di rispondere corretto perché penso che sia probabilmente il migliore generale approccio a una gerarchia un po 'complessa. Tuttavia, non ho usato il suo suggerimento nel mio codice finale.

Sto postando questa nuova risposta per aiutare i futuri utenti SO che cercano questa domanda a causa di un problema simile.

Mentre le interfacce sono probabilmente la migliore soluzione generale per qualcosa di più chiaramente gerarchico, in questo caso non hanno funzionato per me. Non sarei stato in grado di rilasciare questo come un progetto OSS senza usare un nome anonimo se avessi fatto un raggruppamento gerarchico. I nomi delle interfacce implicite stavano iniziando ad avere un cattivo odore, come "IGeneralVideoAudioTextImageMenuCommon" e "IVideoAudioTextImageMenuCommon"; le dichiarazioni di successione con 4 o 5 di questi erano poco elegante del calibro di cui l'umanità non si era visto in precedenza:

///<summary>Represents a single video stream.</summary> 
    public sealed class VideoStream : Media, IGeneralVideoAudioTextImageMenuCommon, 
    IGeneralVideoAudioTextMenuCommon, IVideoAudioTextImageMenuCommon, 
    IVideoTextImageCommon, /* ...ad nauseum. */ 
    { 
     // What would you even name these variables? 
     GeneralVideoAudioTextImageMenuCommon gvatimCommon; 
     GeneralVideoAudioTextMenuCommon gvatmCommon; 
     VideoAudioTextImageMenuCommon vatimCommon; 
     VideoTextImageCommon vticCommon; 

     public VideoStream(MediaInfo mi, int id) { 
     gvatimCommon = new GeneralVideoAudioTextImageMenuCommon(mi, id); 
     gvatmCommon = new GeneralVideoAudioTextMenuCommon(mi, id); 
     vatimCommon = new VideoAudioTextImageMenuCommon(mi, id); 
     vticCommon = new VideoTextImageCommon(mi, id); 
     // --- and more. There are so far at least 10 groupings. 10! 
     /* more code */ 
    } 

La denominazione, uno dei due problemi difficili in informatica Secondo Phil Karlton, è davvero avvicinando una definizione più filosofica di "è un". Una macchina "è un" veicolo, e sì tecnicamente anche una macchina "è un" MotorcycleCarTruckOrAirplane, ma questo è tutto per permettere agli umani di capire il codice, giusto?

Ho considerato un approccio ibrido, in cui i raggruppamenti con un piccolo insieme di funzionalità sarebbero stati uniti. Ma poi mi sono chiesto: "questo rende il codice più facile da capire?" La mia risposta è stata "No", perché vedere una gerarchia dell'eredità implica una certa organizzazione del codice per il lettore. Un secondo metodo probabilmente aggiungerebbe più complessità di quello che è stato salvato: non una vittoria.

La mia soluzione, anche se credo ancora uno migliore deve esistere, è quello di mettere tutte le funzionalità comuni a più di un flusso in una sola classe, "MultiStreamCommon", raggruppati #regions utilizzando internamente:

public class MultiStreamCommon : Media 
{ 
    public MultiStreamCommon(MediaInfo mediaInfo, StreamKind kind, int id) 
     : base(mediaInfo, id) { 
      this.kind = kind; 
    } 

    #region AllStreamsCommon 
    string _format; 
    ///<summary>The format or container of this file or stream.</summary> 
    ///<example>Windows Media, JPEG, MPEG-4.</example> 
    public string format { /* implementation */ }; 

    string _title; 
    ///<summary>The title of the movie, track, song, etc..</summary> 
    public string title { /* implementation */ }; 

    /* more accessors */ 
    #endregion 

    #region VideoAudioTextCommon 
    /* Methods appropriate to this region. */ 
    #endregion 
    // More regions, one for each grouping. 
} 

ogni flusso crea un'istanza di MultiStreamCommon ed espone le funzionalità rilevanti attraverso funzioni di accesso:

public sealed class VideoStream : Media 
{ 
    readonly MultiStreamCommon streamCommon; 

    ///<summary>VideoStream constructor</summary> 
    ///<param name="mediaInfo">A MediaInfo object.</param> 
    ///<param name="id">The ID for this audio stream.</param> 
    public VideoStream(MediaInfo mediaInfo, int id) : base(mediaInfo, id) { 
     this.kind = StreamKind.Video; 
     streamCommon = new MultiStreamCommon(mediaInfo, kind, id); 
    } 

    public string format { get { return streamCommon.format; } } 
    public string title { get { return streamCommon.title; } } 
    public string uniqueId { get { return streamCommon.uniqueId; } } 
    /* ...One line for every media function relevant to this stream type */ 
} 

il vantaggio è che la bruttezza, mentre ancora lì, è limitata alla classe MultiStreamCommon. Credo che i raggruppamenti #region siano molto più leggibili di ogni classe stream che ha sei o più interfacce ereditate. Come fa un manutentore a sapere dove aggiungere una nuova funzione MediaInfo? Basta capire quali flussi si applica e metterlo nella regione appropriata. Se misgroup, funziona ancora e può essere visto attraverso un accessor.

Lo svantaggio è che i flussi multimediali non ereditano la funzionalità appropriata - gli accessor devono essere scritti. Anche la documentazione non viene ereditata, quindi ogni stream avrà bisogno di un tag per ogni accessor, che porterà alla documentazione duplicata. Detto questo: La documentazione duplicata è migliore del codice duplicato.

forzare una gerarchia di ereditarietà in questo caso è molto più complessa di quanto il problema di fondo, che dobbiamo ricordare, è solo avvolgendo qualche libreria.

Per chi è interessato al codice associato a questi thread o al suo scopo (ottenendo facilmente le informazioni sui file multimediali tramite .NET), la pagina Codice Google per questo progetto è here. Per favore fatemi sapere di qualsiasi pensiero, soprattutto da parte di coloro che hanno pubblicato le grandi quantità di codice bello che ho incontrato (Jon Skeet, Marc Gravell e altri).

Problemi correlati