Jason. Cała informatyka w jednym miejscu!

Zapoznam Was dzisiaj z interfejsami! Język C# tak jak każdy inny szanujący się język obiektowy, posiada możliwość programowania interfejsów, które w połączeniu z klasami dają rewelacyjnie spójny kod. Wystarczy tylko wstawić "interface" w języku C# i pierwszy krok już macie za sobą! Pozostaje tylko opanować kilka zasad zanim ten krok się postawi. Na tym także się skoncentrujemy.

"INTERFACE" W JĘZYKU C#. KOLEJNY AKTOR NA TEJ SAMEJ SCENIE

Żeby był sens uczenia się tego wszystkiego, trzeba się najpierw dowiedzieć co to jest, po co jest to wykorzystywane i w jakich okolicznościach.

DEFINICJA

Drodzy moi. Interfejs w najprostszym tłumaczeniu to jest zestaw metod abstrakcyjnych. Ich rolą jest dostarczanie nagłówków metod dla poszczególnych klas i wymuszanie na nich określenia implementacji. Kiedy klasa korzysta z interfejsu, mówimy wtedy że go implementuje. W kontekście klasy występuje dziedziczenie, a implementacja dotyczy interfejsu, dobrze :)?

Interfejsy mają dużo wspólnego z opisanymi niedawno klasami abstrakcyjnymi i samą zasadą abstrakcji, więc poproszę Was o przeczytanie najpierw tego, zanim zaczniecie czytać o dzisiejszym temacie.

ZASTOSOWANIE

Najważniejsze to wiedzieć KIEDY zastosować interfejs zamiast klasy. Wtedy, gdy widzimy taką zależność, że obie różne klasy nie łączy żadna relacja w rozumieniu dziedziczenia, natomiast mają wspólne zachowanie. A zachowanie jak pamiętamy, wywodzi się z metod wbudowanych w klasę. I dlatego powinien wtedy wkroczyć interfejs!

Jaka relacja występuje pomiędzy ścianą, a przedmiotem? Żadna! Popatrzmy jednak na zachowanie. Sytuacja uszkodzenia (mało przyjemny przykład, jednak dobrze obrazujący o co chodzi). Można uszkodzić ścianę? Można. A przedmiot? Też. Mamy wspólne zachowanie! Dlatego widząc, że można uszkodzić zarówno ścianę, jak i przedmiot, wysoce wskazanym będzie zaimplementowanie przez obie klasy interfejsu dostarczającego abstrakcyjną metodę "TakeDamage", a później zdefiniować niestandardowe instrukcje dla jednego i drugiego przypadku (zachowanie takie samo, ale reakcja inna).

Tak właśnie działa interfejs. Dostarcza zestaw metod abstrakcyjnych dla klas, których może nie łączyć żadna relacja w hierarchii dziedziczenia, ale łączą ich wspólne zachowania. Stosując postulat polimorfizmu w wyżej wymienionej sytuacji, jesteśmy w stanie łączyć obiekty w postaci zbioru danych takiego jak tablica, której typem danych może być...interfejs :O!

Sposób użycia jest prosty. Zaraz po zdefiniowaniu sobie nowego interfejsu przy użyciu słowa "interface" w języku C#:

interface IInterface
{
	void DoSomething();
}

wystarczy go "podpiąć" do dowolnej klasy, aby zaczęła go implementować. W przeciwieństwie do języka Java, który wymaga wprowadzenia kolejnego słowa kluczowego ("implements"), w C# uproszczono manewr i podajemy ten sam dwukropek jak przy dziedziczeniu:

class MyClass : IInterface
{
	public void DoSomething()
	{
		// przesłonięcie metody albo błąd kompilacji!
	}
}

W momencie wstawienia powyższej deklaracji, że klasa ma implementować nasz interfejs, jesteśmy odpowiedzialni za zdefiniowanie instrukcji dla WSZYSTKICH pochodzących od niej metod. Jeśli klasa dziedziczy od drugiej, oddzielamy nazwę klasy od interfejsu przecinkiem:

class MyChildClass : MyBaseClass, IInterface
{
	public void DoSomething()
	{
		// przesłonięcie metody albo błąd kompilacji!
	}
}

Klasa może implementować wiele interfejsów, w odróżnieniu od klasy, która może mieć tylko jednego "rodzica". Szczegóły zablokowania możliwości dziedziczenia wielokrotnego opisuję w dalszej części artykułu.

KONWENCJA NAZEWNICZA

Trzeba jeszcze dopisać o panującej w "CSharpie" konwencji nazewniczej dla interfejsów. Najlepiej (czyt. nieobowiązkowe, ale mile widziane), żeby nazwy były anglojęzyczne, zaczynały się od wielkiej litery 'I' i kończyły się na "-ible" lub "-able", aby było jasne do wywnioskowania co ten interfejs daje. Na przykład jeśli klasa miałaby być "odnawialna", wtedy najlepszą nazwą byłoby "IRecoverable".

PRZYKŁAD KODU ŹRÓDŁOWEGO

Różnie w życiu bywa z rozumieniem przy użyciu samego języka naturalnego, dlatego też przytoczoną sytuację zaprogramuję w kodzie źródłowym. Oto jak "interface" w języku C# wpływa na zachowanie programu:

INTERFEJS "IDAMAGEABLE"
interface IDamageable
{
	void TakeDamage();
}
KLASA "ITEM"
class Item : IDamageable
{
	public void TakeDamage()
	{
		Console.WriteLine("Uszkodzenie przedmiotu sprawi, że nie będzie się nadawał do niczego");
	}
}
KLASA "WALL"
class Wall : IDamageable
{
	public void TakeDamage()
	{
		Console.WriteLine("Uszkodzenie ściany powoduje powstawanie pęknięć.");
	}
}
KLASA "PROGRAM"
Item item = new Item();
Wall wall = new Wall();
IDamageable[] damageables = {item, wall};

foreach (IDamageable damageable in damageables)
{
	damageable.TakeDamage();
}

Po uprzednim przygotowaniu sobie interfejsu i dwóch klas go implementujących, tworzymy sobie obu przedstawicieli swoich klas i zaraz po niej tablicę, aby ukazać moim czytelnikom co nam daje polimorfizm zastosowany dla interfejsu. W wyniku nałożonego przez interfejs obowiązku implementacji metody o tej samej nazwie, możemy wykorzystać ten wspólny protokół i elegancko podstawić każdy z obiektów implementujących do tablicy (albo do innego zbioru danych) i wywołać każdą z wersji przy użyciu pętli "foreach" albo jakiejś innej.

INTERFEJSY MOGĄ MIEĆ WŁAŚCIWOŚCI!

Lećmy dalej! "interface" w języku C# ma prawo mieć w sobie nie tylko metody, ale także właściwości :D! Tak, te same "gettery" i "settery" stanowiące udogodnienie dla metod, których jedynym zadaniem miałoby być pobranie i przypisanie wartości. Popatrzcie:

interface IDamageable
{
	int Health {get; set;}
}

Pamiętajcie, że to samo musi się znaleźć potem w każdej bez wyjątku klasie, która ten interfejs implementuje. Więcej wyjaśniać nie trzeba. Chyba, że modyfikatory dostępu, to schodźcie niżej.

W INTERFEJSIE WSZYSTKO JEST PUBLICZNE!

Domyślnym modyfikatorem dostępu dla interfejsów jest "internal", tak jak w przypadku klas. Zaś co do składowych interfejsu, to one zawsze są publiczne ("public"). Wynika to z faktu, iż interfejsy mają "naturę" posiadania samych składowych abstrakcyjnych, które siłą rzeczy muszą zostać upublicznione, tak aby były dostępne dla implementujących go klas (żeby je zdefiniować) oraz na zewnątrz (żeby z nich skorzystać). Myślę, że z tego samego powodu zmienne w interfejsie są niedopuszczalne (mały by był z nich użytek, gdyby mogły być jedynie publiczne - tłumaczyłem dlaczego), ale wolno już wstawić właściwości.

DZIEDZICZENIE INTERFEJSÓW

Interfejs może dziedziczyć od drugiego interfejsu, tak jak klasa od innej klasy. Mało tego, robi się to bardzo łatwo, właściwie...tak samo :P:

interface IInterfaceA
{
	void DoSomethingFromA();
}

interface IInterfaceB : IInterfaceA
{
	void DoSomethingFromB();
}

Domyślacie się ile wtedy trzeba będzie zdefiniować metod w klasie implementującej taki interfejs, który dziedziczy od innego?

class MyClass : IInterfaceB
{
	public void DoSomethingFromA()
	{
		// przesłonięcie metody albo błąd kompilacji!
	}
	
	public void DoSomethingFromB()
	{
		// przesłonięcie metody albo błąd kompilacji!
	}
}

Dwie, nie inaczej...

WIELE INTERFEJSÓW POD RZĄD!

Klasa ma prawo także implementować wiele osobnych interfejsów, tak samo wymienianych po przecinku jak ukazałem na górze:

class MyClass :  IInterfaceA, IInterfaceB
{
	public void DoSomethingFromA()
	{
		// przesłonięcie metody albo błąd kompilacji!
	}
	
	public void DoSomethingFromB()
	{
		// przesłonięcie metody albo błąd kompilacji!
	}
}

Wtedy także każdy interfejs będzie wymagać od Was umieszczenia definicji wszystkich metod naraz. Coś jak iloczyn zbioru w matematyce.

NIE TAK PRĘDKO!

Poznawszy tajniki słowa "interface" w języku C#, muszę Was przestrzec przed paroma zagrożeniami jakie mogą na Was czyhać.

KONFLIKT NAZW PRZEZ DWA / WIELE INTERFEJSÓW

Pierwszy rodzaj problemu na jaki możecie natrafić jest pojawienie się konfliktu nazw przez metodę, która się tak samo nazywa i w jednym, i w drugim interfejsie:

interface IInterfaceA
{
	void DoSomething();
}

interface IInterfaceB
{
	void DoSomething();
}

a klasa implementuje oba:

class MyClass : IInterfaceA, IInterfaceB
{
	???
}

Co wtedy? To zależy od nagłówków metod:

  • kiedy metoda o tej samej nazwie ma ten sam typ zwracanej wartości i tę samą listę parametrów, to definiuje się TYLKO JEDNĄ taką metodę,
  • kiedy metoda o tej samej nazwie różni się typem zwracanej wartości ORAZ listą parametrów, wtedy definiuje się WSZYSTKIE przeciążenia metody,
  • kiedy metoda o tej samej nazwie różni się jedynie typem zwracanej wartości, NIE DA SIĘ przeciążyć metody (albo pierwszy interfejs, albo drugi) i wystąpi błąd kompilacji.

Wspomniany problem może wystąpić także przy dziedziczeniu interfejsu przez inny interfejs.

DLACZEGO INTERFEJSÓW MOŻE BYĆ WIELE, A KLASA TYLKO JEDNA?

Na koniec, odkrycie tajemnicy dlaczego tylko interfejsy nie są ograniczone do jednej sztuki, w przeciwieństwie do klasy. Powodem jest ochrona programisty przed sytuacją niekorzystną. Sytuacją, w której dwie metody o tej samej sygnaturze pochodzą z dwóch różnych klas. Potem bądź programie mądry którą wersję metody wykonać :D. Czy tę pochodzącą od klasy A, czy tę pochodzącą od klasy B? Na jakiej podstawie wybrać jedną z nich? Przecież wylosować nie wolno.

Historia zobrazowana powyżej ukazuje tak zwany "problem śmiertelnego rombu". To jest główny powód dlaczego dziedziczenie wielokrotne zostało w języku C# zamknięte (tak zresztą jak w Javie).

Słowo kluczowe "interface" w języku C#

Interfejs to prostymi słowy zestaw abstrakcyjnych składowych. W języku C#, interfejsy oprócz metod mają prawo mieć również właściwości.


Opowiedziałem chyba już o wszystkich aspektach, jeśli chodzi o słowo kluczowe "interface" w języku C# i samą istotę interfejsów. Radzę Wam po przyjacielsku: opanujcie to dobrze. Interfejsy to jeszcze jeden temat, który można traktować jako elementarne podstawy świadomego programowania. Niewskazane jest ignorowanie jego wagi.

PODOBNE ARTYKUŁY