Ci si imbatte in questa frase quando si leggono i motivi di progettazione.
Ma non lo capisco, qualcuno potrebbe spiegarmelo?
Ci si imbatte in questa frase quando si leggono i motivi di progettazione.
Ma non lo capisco, qualcuno potrebbe spiegarmelo?
Risposte:
Le interfacce sono solo contratti o firme e non sanno nulla delle implementazioni.
La codifica in base all'interfaccia indica che il codice client contiene sempre un oggetto Interface fornito da una factory. Qualsiasi istanza restituita dalla fabbrica sarebbe di tipo Interface che qualsiasi classe candidata alla fabbrica deve aver implementato. In questo modo il programma client non è preoccupato per l'implementazione e la firma dell'interfaccia determina quali operazioni possono essere eseguite. Questo può essere usato per cambiare il comportamento di un programma in fase di esecuzione. Ti aiuta anche a scrivere programmi molto migliori dal punto di vista della manutenzione.
Ecco un esempio di base per te.
public enum Language
{
English, German, Spanish
}
public class SpeakerFactory
{
public static ISpeaker CreateSpeaker(Language language)
{
switch (language)
{
case Language.English:
return new EnglishSpeaker();
case Language.German:
return new GermanSpeaker();
case Language.Spanish:
return new SpanishSpeaker();
default:
throw new ApplicationException("No speaker can speak such language");
}
}
}
[STAThread]
static void Main()
{
//This is your client code.
ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
speaker.Speak();
Console.ReadLine();
}
public interface ISpeaker
{
void Speak();
}
public class EnglishSpeaker : ISpeaker
{
public EnglishSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak English.");
}
#endregion
}
public class GermanSpeaker : ISpeaker
{
public GermanSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak German.");
}
#endregion
}
public class SpanishSpeaker : ISpeaker
{
public SpanishSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak Spanish.");
}
#endregion
}
Questo è solo un esempio di base e l'effettiva spiegazione del principio va oltre lo scopo di questa risposta.
Ho aggiornato l'esempio sopra e ho aggiunto una Speaker
classe base astratta . In questo aggiornamento, ho aggiunto una funzione a tutti gli altoparlanti a "SayHello". Tutti gli oratori parlano "Hello World". Quindi questa è una caratteristica comune con una funzione simile. Fai riferimento al diagramma di classe e scoprirai che la Speaker
classe astratta implementa l' ISpeaker
interfaccia e contrassegna Speak()
come astratta il che significa che ogni implementazione di Speaker è responsabile dell'implementazione del Speak()
metodo poiché varia da Speaker
a Speaker
. Ma tutti gli oratori dicono "Ciao" all'unanimità. Quindi nella classe Speaker astratta definiamo un metodo che dice "Hello World" e ogni Speaker
implementazione ne deriverà il SayHello()
metodo.
Considera un caso in cui SpanishSpeaker
non puoi dire Ciao, quindi in quel caso puoi ignorare il SayHello()
metodo per lo spagnolo e sollevare un'eccezione adeguata.
Si noti che non abbiamo apportato modifiche a Interface ISpeaker. E anche il codice client e SpeakerFactory rimangono invariati. E questo è ciò che realizziamo programmando l'interfaccia .
E potremmo ottenere questo comportamento semplicemente aggiungendo un Speaker di classe astratto di base e alcune modifiche minori in ogni implementazione, lasciando così il programma originale invariato. Questa è una caratteristica desiderata di qualsiasi applicazione e la rende facilmente gestibile.
public enum Language
{
English, German, Spanish
}
public class SpeakerFactory
{
public static ISpeaker CreateSpeaker(Language language)
{
switch (language)
{
case Language.English:
return new EnglishSpeaker();
case Language.German:
return new GermanSpeaker();
case Language.Spanish:
return new SpanishSpeaker();
default:
throw new ApplicationException("No speaker can speak such language");
}
}
}
class Program
{
[STAThread]
static void Main()
{
//This is your client code.
ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
speaker.Speak();
Console.ReadLine();
}
}
public interface ISpeaker
{
void Speak();
}
public abstract class Speaker : ISpeaker
{
#region ISpeaker Members
public abstract void Speak();
public virtual void SayHello()
{
Console.WriteLine("Hello world.");
}
#endregion
}
public class EnglishSpeaker : Speaker
{
public EnglishSpeaker() { }
#region ISpeaker Members
public override void Speak()
{
this.SayHello();
Console.WriteLine("I speak English.");
}
#endregion
}
public class GermanSpeaker : Speaker
{
public GermanSpeaker() { }
#region ISpeaker Members
public override void Speak()
{
Console.WriteLine("I speak German.");
this.SayHello();
}
#endregion
}
public class SpanishSpeaker : Speaker
{
public SpanishSpeaker() { }
#region ISpeaker Members
public override void Speak()
{
Console.WriteLine("I speak Spanish.");
}
public override void SayHello()
{
throw new ApplicationException("I cannot say Hello World.");
}
#endregion
}
List
come tipo, è comunque possibile supporre che l'accesso casuale sia veloce chiamando ripetutamente get(i)
.
Pensa a un'interfaccia come a un contratto tra un oggetto e i suoi clienti. Questa è l'interfaccia che specifica le cose che un oggetto può fare e le firme per accedere a quelle cose.
Le implementazioni sono i comportamenti reali. Supponiamo ad esempio di avere un metodo sort (). È possibile implementare QuickSort o MergeSort. Ciò non dovrebbe importare all'ordinamento del codice client, purché l'interfaccia non cambi.
Librerie come l'API Java e .NET Framework fanno un uso intenso delle interfacce perché milioni di programmatori usano gli oggetti forniti. I creatori di queste librerie devono stare molto attenti a non cambiare l'interfaccia con le classi in queste librerie perché interesserà tutti i programmatori che usano la libreria. D'altra parte possono cambiare l'implementazione quanto vogliono.
Se, come programmatore, codifichi l'implementazione, non appena cambia il codice smette di funzionare. Quindi, pensa ai vantaggi dell'interfaccia in questo modo:
Significa che dovresti provare a scrivere il tuo codice in modo che utilizzi direttamente un'astrazione (classe o interfaccia astratta) invece dell'implementazione.
Normalmente l'implementazione viene iniettata nel codice tramite il costruttore o una chiamata al metodo. Quindi, il tuo codice conosce l'interfaccia o la classe astratta e può chiamare tutto ciò che è definito in questo contratto. Quando viene utilizzato un oggetto reale (implementazione dell'interfaccia / classe astratta), le chiamate stanno operando sull'oggetto.
Questo è un sottoinsieme di Liskov Substitution Principle
(LSP), la L dei SOLID
principi.
Un esempio in .NET potrebbe essere la codifica IList
invece di List
o Dictionary
, quindi è possibile utilizzare qualsiasi classe implementabile in modo IList
intercambiabile nel codice:
// myList can be _any_ object that implements IList
public int GetListCount(IList myList)
{
// Do anything that IList supports
return myList.Count();
}
Un altro esempio della Biblioteca di classe di base (BCL) è la ProviderBase
classe astratta: questo fornisce alcune infrastrutture e, cosa più importante, significa che tutte le implementazioni del provider possono essere utilizzate in modo intercambiabile se si codifica contro di essa.
Se dovessi scrivere una Car Class nell'era Combustion-Car, allora c'è una grande possibilità di implementare oilChange () come parte di questa classe. Ma quando vengono introdotte le auto elettriche, si sarebbe nei guai in quanto non vi è alcun cambio d'olio per queste auto e nessuna implementazione.
La soluzione al problema è avere un'interfaccia performMaintenance () nella classe Car e nascondere i dettagli all'interno dell'implementazione appropriata. Ogni tipo di automobile fornirebbe la propria implementazione per performMaintenance (). Come proprietario di un'auto, tutto ciò che devi fare è eseguireManutenzione () e non preoccuparti di adattarti in caso di CAMBIAMENTO.
class MaintenanceSpecialist {
public:
virtual int performMaintenance() = 0;
};
class CombustionEnginedMaintenance : public MaintenanceSpecialist {
int performMaintenance() {
printf("combustionEnginedMaintenance: We specialize in maintenance of Combustion engines \n");
return 0;
}
};
class ElectricMaintenance : public MaintenanceSpecialist {
int performMaintenance() {
printf("electricMaintenance: We specialize in maintenance of Electric Cars \n");
return 0;
}
};
class Car {
public:
MaintenanceSpecialist *mSpecialist;
virtual int maintenance() {
printf("Just wash the car \n");
return 0;
};
};
class GasolineCar : public Car {
public:
GasolineCar() {
mSpecialist = new CombustionEnginedMaintenance();
}
int maintenance() {
mSpecialist->performMaintenance();
return 0;
}
};
class ElectricCar : public Car {
public:
ElectricCar() {
mSpecialist = new ElectricMaintenance();
}
int maintenance(){
mSpecialist->performMaintenance();
return 0;
}
};
int _tmain(int argc, _TCHAR* argv[]) {
Car *myCar;
myCar = new GasolineCar();
myCar->maintenance(); /* I dont know what is involved in maintenance. But, I do know the maintenance has to be performed */
myCar = new ElectricCar();
myCar->maintenance();
return 0;
}
Spiegazione aggiuntiva: sei un proprietario di auto che possiede più auto. Ritagli il servizio che desideri esternalizzare. Nel nostro caso vogliamo esternalizzare i lavori di manutenzione di tutte le auto.
Non devi preoccuparti di associare il tipo di auto al fornitore di servizi. Devi solo specificare quando vuoi programmare la manutenzione e invocarla. Una società di servizi appropriata dovrebbe intervenire ed eseguire i lavori di manutenzione.
Approccio alternativo
Invoca il lavoro e lo fai da solo. Qui eseguirai il lavoro di manutenzione appropriata.
Qual è il rovescio della medaglia del secondo approccio? Potresti non essere l'esperto nel trovare il modo migliore per eseguire la manutenzione. Il tuo compito è guidare la macchina e godertela. Non essere nel business di mantenerlo.
Qual è il rovescio della medaglia del primo approccio? C'è il sovraccarico di trovare una compagnia ecc. A meno che tu non sia una compagnia di autonoleggio, potrebbe non valere la pena.
Questa affermazione riguarda l'accoppiamento. Un potenziale motivo per utilizzare la programmazione orientata agli oggetti è il riutilizzo. Quindi, ad esempio, puoi dividere l'algoritmo tra due oggetti collaborativi A e B. Questo potrebbe essere utile per la successiva creazione di un altro algoritmo, che potrebbe riutilizzare l'uno o l'altro dei due oggetti. Tuttavia, quando tali oggetti comunicano (invia messaggi - chiama metodi), creano dipendenze tra loro. Ma se si desidera utilizzare l'uno senza l'altro, è necessario specificare cosa dovrebbe fare un altro oggetto C per l'oggetto A se si sostituisce B. Tali descrizioni sono chiamate interfacce. Ciò consente all'oggetto A di comunicare senza modifiche con un oggetto diverso basandosi sull'interfaccia. L'affermazione che hai citato dice che se prevedi di riutilizzare una parte di un algoritmo (o più in generale un programma), devi creare interfacce e fare affidamento su di esse,
Come altri hanno già detto, significa che il tuo codice chiamante dovrebbe conoscere solo un genitore astratto, NON l'attuale classe di implementazione che farà il lavoro.
Ciò che aiuta a capire questo è il PERCHÉ dovresti sempre programmare su un'interfaccia. Ci sono molte ragioni, ma due delle più facili da spiegare sono
1) Test.
Diciamo che ho il mio intero codice di database in una classe. Se il mio programma conosce la classe concreta, posso solo testare il mio codice eseguendolo realmente contro quella classe. Sto usando -> per indicare "parla con".
WorkerClass -> DALClass Tuttavia, aggiungiamo un'interfaccia al mix.
WorkerClass -> IDAL -> DALClass.
Quindi DALClass implementa l'interfaccia IDAL e la classe operaia chiama SOLO tramite questo.
Ora, se vogliamo scrivere test per il codice, potremmo invece creare una semplice classe che si comporta come un database.
WorkerClass -> IDAL -> IFakeDAL.
2) Riutilizzo
Seguendo l'esempio sopra, diciamo che vogliamo passare da SQL Server (che utilizza la nostra DALClass concreta) a MonogoDB. Ciò richiederebbe un lavoro importante, ma NON se avessimo programmato un'interfaccia. In tal caso, scriviamo solo la nuova classe DB e cambiamo (via factory)
WorkerClass -> IDAL -> DALClass
per
WorkerClass -> IDAL -> MongoDBClass