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:
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.