Interfejsy

msm

Definicja

Czasami zachodzi potrzeba traktowania różnych (często niemających ze sobą prawie nic wspólnego) obiektów w podobny sposób.
W językach obiektowych można uzyskać podobny do używania interfejsów efekt dzięki dziedziczeniu. Jednak to wymaga stworzenia konkretnej hierarchii klas, a nie zawsze da się stwierdzić wystarczającą ilość cech wspólnych. Na przykład malować farbami można równie dobrze ściany w pokoju, obraz jak i na przykład paznokcie, ale stworzenie jakiejś klasy abstrakcyjnej po której 'dziedziczyłyby' paznokcie, obraz i ściana jest niezbyt wykonalne... i absurdalne.

Wtedy właśnie w pomoc przychodzą interfejsy. Informują one że pewna grupa klas implementuje pewne metody, właściwości, zdarzenia oraz indeksery, ale nic więcej. Nie można w nich na przykład umieszczać deklaracji zmiennych ani konstruktorów. Implementacja metod i innych elementów interfejsu zależy wyłącznie od obiektu.

Czas skończyć z jałową teorią i zaprezentować jakiś przykład.

Praktyka

Skorzystamy z przykładu jaki został podany wyżej, czyli malowania.

Rekonesans
Najpierw zastanówmy się czego potrzebujemy. Chcemy żeby interfejs umożliwiał nam 'pomalowanie' obiektu. Pierwszym krokiem więc powinno być wybranie metod koniecznych do rozwiązania problemu.

Prototyp
Doszliśmy do wniosku że potrzebujemy tylko jednej metody: Maluj() do kolorowania obiektu. W interfejsie może być oczywiście dowolna ilość metod, ale dla upraszczenia będziemy operować tylko na jednej. Nie musi ona nic zwracać więc będzie typu void. Możemy już napisać prototyp interfejsu:

interface IPaintable
{
    void Maluj();
}

Przyjęte jest aby nazwę interfejsu poprzedzać wielką literką 'I'. Nie jest to obowiązkowe, ale tego zwyczaju warto się trzymać.
Przed metodami nie musimy pisać 'public' gdyż wszystkie metody w interfejsie domyślnie są publiczne (napisanie jakiegokolwiek modyfikatora dostępu spowoduje błąd kompilacji).

Sygnatury
Mamy już definicje metod, ale metodzie Maluj() czegoś brakuje. Oczywiście - metoda powinna przyjmować za parametr farbę którą będzie malować. Zmodyfikujmy więc przykład.

interface IPaintable
{
    void Maluj(Farba f);
}

Warto pamiętać że interfejs 'dopasowuje' tylko metody które mają taką samą syganturę. Czyli jako metoda interfejsu zostanie zaliczona:

public void Zmyj();

ale taka już nie:

public void Zmyj(bool b)

Implementacja
Skoro nasz interfejs jest gotowy, możemy również stworzyć klasę go implementującą.

class Ściana :IPaintable
{
    public Color kolorŚciany;

    public void Maluj(Farba farba)
    {
        kolorŚciany = farba.Kolor;
    }
}

Jak widać po nazwie klasy dodajemy, jak przy dziedziczeniu, dwukropek i nazwę interfejsu.

Użycie
Takiej klasy możemy dalej używać tak samo, ale możemy używać jej również jako instancji interfejsu. Co to oznacza? Oznacza to że jeśli jakaś metoda nie potrzebuje dokładnie jakiegoś obiektu tylko którejś z jego możliwości to... jesteśmy w domu - właśnie do takich celów zostały stworzone interfejsy.

Rozważmy kod:

class Malowanie
{
    public Farba farba;

    public void PomalujŚcianę(Ściana wall)
    {
        wall.Maluj(farba);
    }
}

Wszystko ładnie podziała, ale użytkownik może sobie zażyczyć żeby mógł malować nie tylko ściany, ale i obrazy. Można rozszerzyć klasę Malowanie aby możliwe były obie czynności:

class Malowanie
{
    public Farba farba;

    public void PomalujŚcianę(Ściana wall)
    {
        wall.Maluj(farba);
    }

    public void PomalujObraz(Obraz obr)
    {
        obr.Maluj(farba);
    }
}

Ale w ten sposób będziemy zmuszeni coraz bardziej kopiować kod (a co jeśli będzie potrzeba malowania 300 różnych obiektów?).

Zamiast tego zmodyfikujemy nieco sygnaturę naszej metody:

class Malowanie
{
    public Farba farba;

    public void PomalujCoś(IPaintable thing)
    {
        thing.Maluj(farba);
    }
}

Jak widać metoda nie przyjmuje konkretnego obiektu, a jedynie obiekt implementujący interfejs IPaintable. O przydatności tego rozwiązania chyba nie trzeba nikogo przekonywać.

Epilog i kilka uwag

W ten sposób stworzyliśmy może i niezbyt przydatny w rzeczywistych problemach interfejs, ale na pewno działający. Interfejsy są używane w wysokopoziomowych językach programowania praktycznie wszędzie, więc czas jaki poświęcisz na ich zrozumienie nie pójdzie na marne.

Oraz kilka rzeczy na które warto zwrócić uwagę:

  • Nie przypominasz sobie żebyś kiedyś używał interfejsu? W rzeczywistości bardzo dużo metod których używasz codziennie jest częścią jakiegoś interfejsu. Jako przykład: interfejs IComparable i metoda Compare używana przy porównaniach. interfejs ICloneable klonujący obiekt. Interfejs IDispoidable mówiący że obiekt można zwolnić aktywnie. I wiele innych.
  • W rzeczywistości, jak pewnie zauważyło wiele osobników płci żeńskiej czytających przykłady w artykule, nasz przykład nie jest do końca zgodny z rzeczywistością. Jakby nie patrzeć, paznokci nie maluje się farbą do ścian, a obrazów lakierem! Ładnym rozwiązaniem tego problemu byłoby stworzenie kolejnego interfejsu "IFarba" i zmienienie sygnatury metody Maluj() żeby z niej korzystała.
  • Z interfejsami wiąże się wiele ciekawych sztuczek i technik jak np. wzorzec projektowy factory.

7 komentarzy

Czy problem lakieru i farby mógłby być rozwiązany tak?

using System;

public enum Kolor
{
	Zielony,
	Czerwony,
	Niebieski,
	Czarny,
	Biały
}

interface IFarba
{
	void PodajKolor(Kolor k);
}

interface IPaintable
{
	void Maluj(IFarba f);
}

class Malowanie
{
	public void Pomaluj(IPaintable thing, IFarba typFarby)
	{
		thing.Maluj(typFarby);
	}
}

class Farba : IFarba
{
	public Kolor KolorFarby;
	public void PodajKolor(Kolor k)
	{
		this.KolorFarby = k;
	}
}

class Lakier : IFarba
{
	public Kolor KolorLakieru;
	public void PodajKolor(Kolor k)
	{
		this.KolorLakieru = k;
	}
}

class Samochód : IPaintable
{
	public void Maluj(IFarba l)
	{
		Console.WriteLine("Maluję samochód na kolor " + (l as Lakier).KolorLakieru);
	}
}

class Ściana : IPaintable
{
	public void Maluj(IFarba f)
	{
		Console.WriteLine("Maluję ścianę na kolor " + (f as Farba).KolorFarby);
	}
}

public class Program
{
	static void Main()
	{
		Malowanie m = new Malowanie();
		Lakier l = new Lakier();
		Farba f = new Farba();
		Samochód s = new Samochód();
		Ściana ś = new Ściana();
		l.PodajKolor(Kolor.Czerwony);
		f.PodajKolor(Kolor.Zielony);
		// Pomaluj samochód lakierem:
		m.Pomaluj(s, l);
		// Pomaluj ścianę farbą:
		m.Pomaluj(ś, f);
	}
}

Nie cierpie interfejsów są bezużyteczne...

@down: niekonsekwencja nazw w tutorialu :P zamiast .Pomaluj(...) powinno byc uzyte .Maluj(...) ;]

Witam,
Mam pytanie co do kodu tego z tym interfersjem.
Co mnie jako początkującego programisty zraża do interferjsu.
A mianowicie, skąd klasa Malowanie wie o metodzie 'pomaluj' jeśli w kodzie nie jest określone dla niej dziedziczenie z interferjsu ?

Przez ten problem właśnie mam problem ze zrozumieniem tego interfejsu.

Pozdrawiam,
black_man

No to cieszę się że przynajmniej jednej osobie pomogłem ;)

chyba nic. Jako początkujący programista zajrzałem tu przypadkiem, ale wydaje mi się, że zrozumiałem o co chodzi :D

Co ja mam z głową, żeby na taki nieskomplikowany temat pisać epopeje? ;(