Modellazione di un dispositivo di comunicazione multicanale in C #

2

Sfondo

Noi (i miei colleghi ed io) stiamo architettando un'API C # di alto livello per interagire con la DLL di Windows nativa di un dispositivo hardware.

Per prima cosa discuterò l'architettura dell'hardware, quindi passerò alla parte dell'architettura del software.

Dispositivo principale

Il dispositivo ha i seguenti componenti:

  • Esattamente 10 canali di ricezione

  • Esattamente 4 canali di trasmissione

Ricevi canali

È possibile solo il numero esatto specificato di canali di ricezione sul dispositivo in base alle specifiche hardware (né più né meno). I canali di ricezione mantengono i propri buffer e vari getter / setter per le loro configurazioni hardware interne.

Ho deciso di dichiarare i canali come una matrice di 10 elementi pubblica all'interno della classe "dispositivo principale", per i seguenti motivi:

  • Ogni volta che vengono ricevuti nuovi dati, controlliamo gli indirizzi su tutti i canali nel "dispositivo principale" per sapere a quale canale sono destinati i dati ricevuti. Questo è attualmente fatto in un ciclo foreach che è bello e pulito.
  • Quando assegni le proprietà dei canali, posso farlo facilmente usando un ciclo foreach (di nuovo, fornisce pulizia).
  • Devi accedere all'oggetto reale, non solo restituire un'istanza dell'oggetto.

Trasmetti canali

Ho deciso di dichiarare i canali come una matrice pubblica di 4 elementi all'interno della classe "dispositivo principale", per gli stessi motivi dei ricevere canali.

problema

  1. I canali sono esposti pubblicamente.
  2. Come uno dei miei colleghi ha chiesto, "cosa succede se qualcuno inserisce un indice non valido nella matrice?"

Domanda

L'array è il modo corretto di fare questo? C'è un altro modo? Sto cercando alcuni suggerimenti sull'architettura generale.

Promemoria: Ho bisogno di accedere direttamente agli oggetti del canale, tuttavia lo facciamo.

    
posta Snoop 11.03.2016 - 19:50
fonte

2 risposte

3

Credo che dovresti mantenere la quantità di canali astratti:

  • La quantità di canali può variare in futuro, ad es. up / downgrade hardware (nuovi dispositivi)

  • Potresti voler prenotare alcuni canali in futuro, ad es. per comunicazioni fuori banda e notifiche

L'esattezza dovrebbe essere mantenuta e assicurata durante l'inizializzazione.

Devi esporre i canali con il tipo meno specifico possibile, come IEnumerable<ReceivingChannel> e IEnumerable<TransmittingChannel> .

Il MainDevice dovrebbe esporre un modo per inizializzare o gestire queste raccolte, hai diverse opzioni non esclusive:

  • Parametri costruttore
private List<IReceivingChannel> receivingChannels = new List<IReceivingChannel>();

private List<IReceivingChannel> transmittingChannels = new List<ITransmittingChannel>();

public MainDevice(IEnumerable<IReceivingChannel> receivingChannels,
                  IEnumerable<ITransmittingChannel> transmittingChannels)
{
    this.receivingChannels.AddRange(receivingChannels);
    this.transmittingChannels.AddRange(transmittingChannels);
}
  • Metodi di inizializzazione del fluido
public MainDevice WithReceivingChannels(IEnumerable<IReceivingChannel> receivingChannels)
{
    this.receivingChannels.AddRange(receivingChannels);
    return this;
}

public MainDevice WithReceivingChannels(params IReceivingChannel[] receivingChannels)
{
    return WithReceivingChannel((IEnumerable<IReceivingChannel>)receivingChannels);
}

public MainDevice WithTransmittingChannels(IEnumerable<ITransmittingChannel> transmittingChannels)
{
    this.transmittingChannels.AddRange(transmittingChannels);
    return this;
}

public MainDevice WithTransmittingChannels(params ITransmittingChannel[] transmittingChannels)
{
    return WithTransmittingChannels((IEnumerable<ITransmittingChannel>)transmittingChannels);
}

Forse lancia un'eccezione (ad esempio InvalidOperationException ) nel caso in cui usi questi metodi dopo che l'oggetto è stato utilizzato per la prima volta da metodi di lavoro effettivi. O meglio ancora, avere questi metodi in una classe builder, dove finalmente si chiama GetDevice che creerà e inizializzerà il dispositivo, possibilmente con un'iniezione di dipendenza (non discuterò di DI qui.)

  • Metodi

    AddReceivingChannel , RemoveReceivingChannel , AddTransmittingChannel , RemoveTransmittingChannel

    GetReceivingChannels che restituisce IEnumerable<IReceivingChannel> o una proprietà ReceivingChannels simile

    GetTransmittingChannels che restituisce IEnumerable<ITransmittingChannel> o una proprietà TransmittingChannels simile

    GetReceivingChannelById , GetReceivingChannelByAddress , GetTransmittingChannelById , GetTransmittingChannelByAddress

Non utilizzare le proprietà modificabili, in quanto ciò richiede problemi. Il MainDevice dovrebbe essere l'unica entità responsabile della gestione della sua composizione effettiva.

In questo modo, puoi iniziare usando gli array o gli elenchi per memorizzare i canali, ma passa ai dizionari o agli alberi (ad esempio SortedDictionary<TKey, TValue> ) se accedervi con una chiave diventa un collo di bottiglia. E gli utenti di MainDevice sono altrettanto felici.

Puoi definire un'interfaccia di base IChannel per le cose comuni che tutti i canali fanno, come aprire e / o chiudere con un timeout opzionale, e avere IReceivingChannel e ITransmittingChannel contenere le operazioni specifiche, come ricevere e trasmettendo con un timeout opzionale:

public interface IChannel
{
    void Open(TimeSpan timeout);
    void Close(TimeSpan timeout);
}

// I'm using generics, but you don't have to if you'll always
// deal with the same data type, e.g. IEnumerable<byte>
//
// I'm also over-simplifying by not stating end-of-transmission
// and other usual channel state.

public interface IReceivingChannel<out T> : IChannel
{
    T Receive(TimeSpan timeout);
}

public interface ITransmittingChannel<in T> : IChannel
{
    void Transmit(T obj, TimeSpan timeout);
}

Nel caso in cui sia necessario accedere ad alcuni oggetti interni da un canale, definire interfacce specifiche su cui è possibile utilizzare l'operatore as anziché la classe effettiva. Ad esempio, un'interfaccia IFrobberContainer con un metodo GetNativeFrobber (o una proprietà NativeFrobber simile), che fa non estendi IReceivingChannel .

In questo modo, qualsiasi oggetto può contenere uno sfarfallio:

public interface IFrobberContainer
{
    IFrobber NativeFrobber { get; }
}

internal class FrobberReceivingChannel : IReceivingChannel<Thing>, IFrobberContainer
{
    public void Open(TimeSpan timeout) { /* ... */ }

    public void Close(TimeSpan timeout) { /* ... */ }

    public Thing Receive(TimeSpan timeout) { /* ... */ }

    public IFrobber NativeFrobber { get; private set; }
}

internal class FrobberTransmittingChannel : ITransmittingChannel<Thing>, IFrobberContainer
{
    public void Open(TimeSpan timeout) { /* ... */ }

    public void Close(TimeSpan timeout) { /* ... */ }

    public void Transmit(Thing obj, TimeSpan timeout) { /* ... */ }

    public IFrobber NativeFrobber { get; private set; }
}

Se hai qualcosa in più da fare con i canali frobber, puoi chiedere se il canale ha uno sfarfallio usando l'operatore as .

Con il nuovo operatore null-condizionale C # 6, puoi fare questo:

(channel as IFrobberContainer)?.NativeFrobber?.Frobulate();

invece di questo:

var frobberContainer = channel as IFrobberContainer;
if (frobberContainer != null)
{
    var frobber = frobberContainer.NativeFrobber;
    if (frobber != null)
    {
        frobber.Frobulate();
    }
}

Separando questi aspetti distinti in interfacce, rendi i tuoi oggetti reali più flessibili, più facilmente sostituibili e mockabili (ottimo per i test di unità).

Se insisti per avere 10 canali di ricezione e 4 canali di trasmissione, probabilmente non dovresti usare le raccolte, ma al contrario usare nomi molto buoni per 14 proprietà. Per l'iterazione generica, puoi mantenere le raccolte interne inizializzate una volta, per evitare la ripetizione ovvia del codice.

Dovresti comunque utilizzare le interfacce e documentare quali implementano ciascun canale, invece di utilizzare le classi effettive come i tipi per queste proprietà, per conservare i benefici di sostituibilità a fini di refactoring, manutenzione e test.

    
risposta data 30.03.2016 - 17:44
fonte
1

Poiché si tratta di un dispositivo hardware, non mi preoccuperei troppo di essere in grado di aggiungere o rimuovere dinamicamente canali, piuttosto lo terrei semplicemente stupido. Come consumatore dell'interfaccia del dispositivo è meglio che acceda in questo modo,

var device = //initialize the device

device.In1.Write(dataIn1);
device.In2.Write(dataIn2);

var out = device.Out1.Read();

quindi l'interfaccia del canale sarà simile a,

IWriteChannel{
   bool Write(object data);
}

IReadChannel{
   object Read();
}

Questa è un'interfaccia piuttosto semplice e, se lo desideri, puoi aggiungere anche dei tipi all'interfaccia. Ad esempio

IWriteChannel{
   bool Write<T>(T data);
}
    
risposta data 31.03.2016 - 02:53
fonte

Leggi altre domande sui tag