Ho ricevuto ottimi riscontri dalle altre risposte al momento in cui ho postato questa domanda. Oggi mi è tornato in mente questo scenario e ho pensato a un paio di modi specifici in cui questo può essere gestito, e me ne sto rendendo conto ora sembra essere molto in linea con le risposte che ho ricevuto prima. Per spiegare questo ho bisogno di fare una distinzione tra l'implementatore e l'istanziatore.
Instantiator (codice esterno) vs Implementor (codice interno)
Mi riferirò all'implementatore della classe Toggler come implementatore e al codice che lo usa e ad un certo punto lo istanzia come instantiator .
L'implementazione della classe Toggler
non dovrebbe essere eccessivamente preoccupata per tutte le combinazioni di modi in cui può essere utilizzata dal codice che la istanzia. Dovrebbe solo fare ciò che ha senso in relazione alla sua implementazione di classe. Spetta all'istanziatore decidere come verrà utilizzato. L'implementatore dovrebbe probabilmente essere consapevole della sua interfaccia e dovrebbe essere pulito / semplice, e come tale sa come può essere usato, ma non è responsabile per tutti i modi in cui sarà usato.
Opzione 1: se separio le interfacce
Diciamo che è implementato come tale:
public class Toggler : IAutoToggler, IManualToggler { ... }
Ora a questo punto possiamo già fare facilmente IAutoToggler autoToggler = new Toggler();
per programmare solo sull'interfaccia di commutazione automatica e lo stesso per la commutazione manuale. Ma la preoccupazione ora è se io volessi entrambi? Questa responsabilità ricade sull'istantaneo. Come menzionato nella risposta di John Wu, questo può essere gestito da un contenitore IoC e con l'iniezione delle dipendenze un modo potrebbe essere quello di iniettare entrambe le interfacce in un costruttore di classi e utilizzare le interfacce separatamente in una singola classe che lo avvolge.
public class TogglerHandler
{
private readonly IAutoToggler _autoToggler;
private readonly IManualToggler _ toggler;
public TogglerHandler(IAutoToggler autoToggler, IManualToggler toggler) {...}
// exposing access to both underlying interfaces here:
public IEnumerable<IToggleable> Toggle(IAttribute attribute) {...}
public void Toggle(IEnumerable<IToggleable> toggles) {...}
}
Un altro modo per farlo è sottoclasse la classe Toggler e fornire la tua interfaccia:
public interface IMixedToggler : IAutoToggler, IManualToggler {}
public class MixedToggler : Toggler, IMixedToggler
{
}
// somewhere using the code now can use a mixed toggling interface
IMixedToggler toggler = new MixedToggler();
Opzione 2: se creo un'interfaccia singola
Fabio ha detto che ha senso usare una singola interfaccia.
Diciamo che l'implementazione è simile a questa e ha i metodi di azione precedentemente in una singola interfaccia:
public interface IToggler
{
IEnumerable<IToggleable> ToggleByAttribute(IAttribute attribute);
void Toggle(IEnumerable<IToggleable> toggles);
}
public class Toggler : IToggler { ... }
Ora l'istanziatore o l'utente di questa interfaccia potrebbe non aver bisogno dell'interfaccia completa. Questa responsabilità di esporre solo ciò che è necessario ricade sull'istanza che utilizzerà l'implementazione di IToggler. Un modo possibile è di riavvolgerlo di nuovo, quindi diciamo che è necessaria solo la commutazione automatica:
public interface IAutoToggler
{
IEnumerable<IToggleable> Toggle(IAttribute attribute)
}
public class AutoToggler : Toggler, IAutoToggler
{
...
}
// somewhere using the code now can only use auto-toggler interface
IAutoToggler autoToggler = new AutoToggler();
Conclusione
Ciò che ho concluso è che non importa se ho separato l'interfaccia o l'ho mantenuta una sola, è ancora possibile esporla o limitarne l'accesso via ereditarietà o avvolgendo il codice. La ragione per cui ho avuto difficoltà a vederlo prima è perché ero sia colui che implementa quella classe sia colui che la crea, ma forse fare questa distinzione è utile per prendere questo tipo di decisioni.
Quando si progetta l'interfaccia di un'implementazione, fare in modo che abbia più senso per il punto di vista dell'implementazione interna, e per il codice esterno chiamarlo o usarlo così com'è se ti dà accesso a ciò di cui hai bisogno o modificarlo a una forma che ha più senso interagire con.
In risposta alla mia domanda iniziale, l'interfaccia IMixedToggler non avrebbe dovuto essere creata / implementata. L'unico scopo sarebbe avere un ulteriore modo di interfacciare con la classe. Questo dovrebbe essere stato calcolato dal codice che l'istanza / utilizza come responsabile che è caduto là nel codice esterno.
Nota a margine
Questo estratto di codice proviene da Http Parser di Kestrel:
public class HttpParser<TRequestHandler>
: IHttpParser<TRequestHandler>
where TRequestHandler
: IHttpHeadersHandler,
IHttpRequestLineHandler
{
...
}
Questo esempio in realtà mi ha portato a concludere che l'implementazione interna non dovrebbe dettare il modo in cui verrà utilizzata. HttpParseObject non può essere inizializzato senza che esista un'interfaccia esistente sia IHttpHeadersHandler che IHttpRequestLineHandler. Quindi, anche per inizializzare questa cosa è necessario avere un'interfaccia che non è nemmeno specificata direttamente qui, dice solo che ha bisogno di un'interfaccia che eredita da entrambi. Quindi, in sostanza, sta applicando che l'instantiator crea una nuova interfaccia conforme a ciò che vuole:
interface IFoo: IHttpHeadersHandler, IHttpRequestLineHandler { }
var parser = new HttpParser<IFoo>(true);