Na to zdanie można natknąć się, czytając o wzorcach projektowych.
Ale ja tego nie rozumiem, czy ktoś mógłby mi to wyjaśnić?
Na to zdanie można natknąć się, czytając o wzorcach projektowych.
Ale ja tego nie rozumiem, czy ktoś mógłby mi to wyjaśnić?
Odpowiedzi:
Interfejsy to tylko umowy lub podpisy i nie wiedzą nic o implementacjach.
Kodowanie względem interfejsu oznacza, że kod klienta zawsze zawiera obiekt interfejsu, który jest dostarczany przez fabrykę. Każda instancja zwrócona przez fabrykę byłaby typu Interface, którą musi zaimplementować każda klasa kandydująca do fabryki. W ten sposób program kliencki nie martwi się o implementację, a podpis interfejsu określa, jakie wszystkie operacje można wykonać. Można to wykorzystać do zmiany zachowania programu w czasie wykonywania. Pomaga również w pisaniu znacznie lepszych programów z punktu widzenia konserwacji.
Oto podstawowy przykład dla Ciebie.
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
}
To tylko podstawowy przykład, a rzeczywiste wyjaśnienie zasady wykracza poza zakres tej odpowiedzi.
Zaktualizowałem powyższy przykład i dodałem abstrakcyjną Speaker
klasę bazową. W tej aktualizacji dodałem funkcję do wszystkich głośników do „SayHello”. Wszyscy mówcy mówią „Hello World”. Jest to więc wspólna cecha o podobnej funkcji. Zapoznaj się z diagramem klas, a zobaczysz, że interfejs Speaker
implementacji klasy abstrakcyjnej ISpeaker
i oznacza Speak()
jako abstrakcyjny, co oznacza, że każda implementacja głośnika jest odpowiedzialna za implementację Speak()
metody, ponieważ różni się od Speaker
do Speaker
. Ale wszyscy mówcy jednogłośnie mówią „Cześć”. Zatem w abstrakcyjnej klasie Speaker definiujemy metodę, która mówi „Hello World”, a każda Speaker
implementacja wyprowadza tę SayHello()
metodę.
Rozważ przypadek, w którym SpanishSpeaker
nie można się przywitać, więc w takim przypadku możesz zastąpić SayHello()
metodę dla języka hiszpańskiego i zgłosić odpowiedni wyjątek.
Należy pamiętać, że nie wprowadziliśmy żadnych zmian w interfejsie ISpeaker. Kod klienta i SpeakerFactory również pozostają niezmienione. I to właśnie osiągamy dzięki programowaniu do interfejsu .
Mogliśmy osiągnąć to zachowanie, po prostu dodając podstawowy głośnik klasy abstrakcyjnej i drobne modyfikacje w każdej implementacji, pozostawiając w ten sposób oryginalny program bez zmian. Jest to pożądana funkcja każdej aplikacji, która ułatwia jej konserwację.
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
jako typu, nadal możesz założyć, że dostęp losowy jest szybki przez wielokrotne wywoływanie get(i)
.
Wyobraź sobie interfejs jako kontrakt między obiektem a jego klientami. Oznacza to, że interfejs określa rzeczy, które obiekt może robić, oraz sygnatury dostępu do tych rzeczy.
Implementacje to rzeczywiste zachowania. Załóżmy na przykład, że masz metodę sort (). Możesz zaimplementować QuickSort lub MergeSort. To nie powinno mieć znaczenia dla kodu klienta wywołującego sort, o ile interfejs się nie zmieni.
Biblioteki, takie jak Java API i .NET Framework, intensywnie wykorzystują interfejsy, ponieważ miliony programistów używają dostarczonych obiektów. Twórcy tych bibliotek muszą bardzo uważać, aby nie zmieniać interfejsu do klas w tych bibliotekach, ponieważ wpłynie to na wszystkich programistów korzystających z biblioteki. Z drugiej strony mogą zmieniać implementację tak bardzo, jak chcą.
Jeśli jako programista kodujesz w oparciu o implementację, to gdy tylko ona się zmieni, Twój kod przestanie działać. Pomyśl więc o zaletach interfejsu w ten sposób:
Oznacza to, że powinieneś spróbować napisać swój kod tak, aby używał abstrakcji (abstrakcyjnej klasy lub interfejsu) zamiast bezpośrednio implementacji.
Zwykle implementacja jest wstrzykiwana do kodu za pośrednictwem konstruktora lub wywołania metody. Zatem Twój kod wie o interfejsie lub klasie abstrakcyjnej i może wywołać wszystko, co jest zdefiniowane w tym kontrakcie. Jako rzeczywisty obiekt (implementacja interfejsu / klasy abstrakcyjnej) jest używany, wywołania działają na obiekcie.
Jest to podzbiór Liskov Substitution Principle
(LSP), L SOLID
zasad.
Przykładem w .NET może być kodowanie za pomocą IList
zamiast List
lub Dictionary
, więc możesz użyć dowolnej klasy, która implementuje IList
zamiennie w kodzie:
// myList can be _any_ object that implements IList
public int GetListCount(IList myList)
{
// Do anything that IList supports
return myList.Count();
}
Innym przykładem z biblioteki klas podstawowych (BCL) jest ProviderBase
klasa abstrakcyjna - zapewnia to pewną infrastrukturę i, co ważne, oznacza, że wszystkie implementacje dostawców mogą być używane zamiennie, jeśli kodujesz przeciwko niej.
Gdybyś miał napisać klasę samochodu w erze samochodów spalinowych, to jest duża szansa, że zaimplementowałbyś oilChange () jako część tej klasy. Ale kiedy zostaną wprowadzone samochody elektryczne, będziesz miał kłopoty, ponieważ nie ma potrzeby wymiany oleju w tych samochodach ani wdrożenia.
Rozwiązaniem problemu jest posiadanie interfejsu performMaintenance () w klasie Car i ukrycie szczegółów wewnątrz odpowiedniej implementacji. Każdy typ samochodu zapewniałby własną implementację funkcji performMaintenance (). Jako właściciel samochodu musisz tylko zająć się utrzymaniem () i nie martwić się o adaptację w przypadku ZMIANY.
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;
}
Dodatkowe wyjaśnienie: jesteś właścicielem samochodu, który posiada wiele samochodów. Wytwarzasz usługę, którą chcesz zlecić na zewnątrz. W naszym przypadku chcemy zlecić obsługę techniczną wszystkich samochodów.
Nie chcesz martwić się o skojarzenie typu samochodu z usługodawcą. Wystarczy określić, kiedy chcesz zaplanować konserwację i ją wywołać. Właściwa firma serwisowa powinna wskoczyć i wykonać prace konserwacyjne.
Alternatywne podejście.
Wzywasz pracę i robisz to sam. Tutaj będziesz wykonywać odpowiednie prace konserwacyjne.
Jakie są wady drugiego podejścia? Możesz nie być ekspertem w znalezieniu najlepszego sposobu wykonania konserwacji. Twoim zadaniem jest prowadzić samochód i cieszyć się nim. Nie zajmować się utrzymaniem go.
Jakie są wady pierwszego podejścia? Istnieje narzut związany ze znalezieniem firmy itp. Jeśli nie jesteś wypożyczalnią samochodów, może to nie być warte wysiłku.
To stwierdzenie dotyczy łączenia. Jednym z potencjalnych powodów używania programowania obiektowego jest ponowne wykorzystanie. Na przykład możesz podzielić swój algorytm na dwa współpracujące ze sobą obiekty A i B. Może to być przydatne do późniejszego utworzenia innego algorytmu, który mógłby ponownie wykorzystać jeden z dwóch obiektów. Jednak gdy te obiekty komunikują się (wysyłają wiadomości - wywołują metody), tworzą między sobą zależności. Ale jeśli chcesz użyć jednego bez drugiego, musisz określić, co powinien zrobić inny obiekt C dla obiektu A, jeśli zastąpimy B. Te opisy nazywane są interfejsami. Umożliwia to obiektowi A komunikację bez zmian z innym obiektem zależnym od interfejsu. Oświadczenie, o którym wspomniałeś, mówi, że jeśli planujesz ponownie użyć jakiejś części algorytmu (lub bardziej ogólnie programu), powinieneś stworzyć interfejsy i polegać na nich,
Jak powiedzieli inni, oznacza to, że kod wywołujący powinien wiedzieć tylko o abstrakcyjnym rodzicu, a NIE o rzeczywistej klasie implementującej, która wykona pracę.
Pomaga to zrozumieć, DLACZEGO zawsze należy programować do interfejsu. Jest wiele powodów, ale dwa z najłatwiejszych do wyjaśnienia to
1) Testowanie.
Powiedzmy, że mam cały kod bazy danych w jednej klasie. Jeśli mój program wie o konkretnej klasie, mogę przetestować mój kod tylko wtedy, gdy naprawdę uruchamiam go na tej klasie. Używam -> do oznaczenia „rozmowy z”.
WorkerClass -> DALClass Jednak dodajmy interfejs do miksu.
WorkerClass -> IDAL -> DALClass.
Więc DALClass implementuje interfejs IDAL, a klasa robocza wywołuje TYLKO przez to.
Teraz, jeśli chcemy napisać testy dla kodu, moglibyśmy zamiast tego stworzyć prostą klasę, która zachowuje się jak baza danych.
WorkerClass -> IDAL -> IFakeDAL.
2) Ponowne użycie
Zgodnie z powyższym przykładem, powiedzmy, że chcemy przejść z SQL Server (z którego korzysta nasza konkretna klasa DAL) do MonogoDB. Wymagałoby to dużej pracy, ale NIE, gdybyśmy zaprogramowali interfejs. W takim przypadku po prostu piszemy nową klasę DB i zmieniamy (przez fabrykę)
WorkerClass -> IDAL -> DALClass
do
WorkerClass -> IDAL -> MongoDBClass