10. Interfejsy, część 1

10. Interfejsy, część 1

 Paweł Kruczkowski
Paweł Kruczkowski
00:00
28.11.2006
101198 wyświetleń

Interfejs to pojęcie bardzo ważne i często używane przez programistów. W niniejszym artykule opowiemy sobie o jego prawidłowej definicji oraz implementacji, jak również spróbujemy pokazać jak obsługiwać kilka interfejsów naraz i jak je rozbudowywać.

Na początek wprowadźmy sobie pojęcie interfejsu. Interfejs – wg najprostszego języka -  to kontrakt między użytkownikiem a klasą (lub strukturą, o której była mowa tydzień temu). Gdy dana klasa (struktura) obsługuje interfejs, to oznacza, że gwarantuje klientowi obsługę metody, właściwości czy też jakiegoś zdarzenia, które zostały wcześniej zdefiniowane w tym interfejsie. Innymi słowy, dzięki interfejsowi wymuszamy na klasie to co ona musi wykonywać, ale oczywiście nie określamy jak ma to robić.

Interfejsy są podobne do klas, ale nie posiadają w swojej definicji egzemplarza, a ich metody deklaruje się bez żadnej treści (podobnie jak np. właściwości etc.). W praktyce możemy definiować sobie interfejsy, które nie będą przyjmować żadnych założeń na temat swojej implementacji. Interfejsy takie mogą być implementowane przez dowolną liczbę klas.

Interfejs jest alternatywą dla klasy abstrakcyjnej, którą już bardzo dobrze znamy. Jak wiemy, klasa abstrakcyjna stanowi bazę metod dla wszystkich jej klas pochodnych i nie można tworzyć jej instancji (próba tworzenia obiektu typu klasy abstrakcyjnej zakończy się zgłoszeniem przez kompilator błędu). Do klas abstrakcyjnych możemy się odwoływać jedynie poprzez mechanizm dziedziczenia oraz polimorfizmu. Podobnie jest z interfejsami za wyjątkiem pewnej, istotnej różnicy. A mianowicie takiej, że klasa abstrakcyjna stanowi klasę bazową dla jej klas pochodnych, a interfejsy możemy dziedziczyć w sposób dowolny (interfejsy nie są jedynie bazą danej klasy).

Klasa abstrakcyjna - zgodnie z definicją - posiada co najmniej jedną metodę abstrakcyjną, ale może mieć w swoim ciele metody nieabstrakcyjne, których to klasa pochodna nie musi przesłaniać (klasa pochodna po klasie abstrakcyjnej musi przesłonić wszystkie metody abstrakcyjne jakie ona posiada). Kiedy natomiast jakaś klasa implementuje interfejs, to wówczas musi udostępniać wszystkie metody, właściwości, czy też zdarzenia jakie zdefiniowaliśmy w tym interfejsie. To jest druga istotna różnica między klasami abstrakcyjmymi a interfejsami.

Po krótkim wstępie spróbujmy zdefiniować sobie interfejs. Składnia definicji interfejsu jest bardzo prosta i wygląda następująco:

           [modyfikatory_dostepu] interface nazwa_interfejsu [: lista_bazowa]
{
           //treść interfejsu
}

Modyfikatory dostępu są przez nas już bardzo dobrze znane. W tym miejscu jedynie je wymienię. A więc: private, public, protected, internal oraz protected internal. Po modifikatorze występuje słowo kluczowe interface, a po nim konkretna nazwa tworzonego interfejsu. Polecamy nazywać interfejsy w ten sposób, żeby zaczynały się od dużej litery „I” (np. IMojInterfejs). Po nazwie interfejsu znajduje się lista bazowa, czyli lista interfejsów, które mogą rozszerzać dany interfejs. O tym mechaniźmie powiemy sobie jeszcze w niniejszym artykule szerzej. W ciele interfejsu znajduje się jego treść, a więc opis metod, właściwości, czy też innych elementów.

Na początek napiszmy więc prosty przykład prezentujący sposób implementowania interfejsów:

interface IMojInterfejs
{
    int Dodawanie();
    int Wynik
    {
        get;
        set;
    }
}
 
public class MojaKlasa : IMojInterfejs
{
    int a, b;
    //przechowuje wartosc wlasciwosci
    private int _wynik = 0;
 
    //konstruktor klasy MojaKlasa
    public MojaKlasa(int a, int b)
    {
        this.a = a;
        this.b = b;
    }
 
    //implementacja metody Dodawanie()
    public int Dodawanie()
    {
        return a + b;
    }
 
    //implementacja wlasciwosci
    public int Wynik
    {
        get { return _wynik; }
        set { _wynik = value; }
    }
}
 
public class Glowna
{
    static void Main()
    {
        MojaKlasa mk = new MojaKlasa(5, 6);
        System.Console.WriteLine("Wynik dodwania wynosi: {0}", mk.Dodawanie());
        mk.Wynik = mk.Dodawanie() - 8;
        System.Console.WriteLine("Wynik odejmowania wynosi: {0}", mk.Wynik);
    }
}

W powyższym przykładzie zdefiniowaliśmy sobie interfejs o nazwie: IMojInterfejs, który w swoim ciele ma zdefiniowaną jedną metodę: Dodawanie() oraz właściwość Wynik, która jest typu intiger. Jak widzimy właściwość ta nie posiada kodu akcesorów: get() oraz set(), a jedynie posiada informacje, że one występują. Podobnie jest ze wspomnianą metodą Dodawanie(), która w ciele interfejsu nie posiada implementacji. Drugą ważną rzeczą, jest fakt, że metoda ta nie posiada modyfikatora dostępu. Niejawnie bowiem metody interfejsów są publiczne, a umieszczenie jakiegokolwiek modyfikatora dostępu spowoduje, że kompilator wygeneruje błąd.

MojaKlasadziedziczy powyższy interfejs, co oznacza, że musi „wypełnić” kontrakt. A więc po prostu w jej strukturze musi znajdować się zarówno implementacja metody Dodawanie() jak również właściwości Wynik. W przeciwnym wypadku kompilator zgłosi nam błąd podczas kompilacji i poinformuje programistę, że jego klasa nie wypełnia kontraktu.

Po skompilowaniu i uruchomieniu powyższego przykładu otrzymamy następujące wyniki:

Klasy mogą dziedziczyć więcej niż jeden interfejs. Na przykład, gdy MojaKlasa ma również umożliwiać mnożenie i dzielenie dwóch liczb całkowitych, to odpowiedzialne za to metody możemy zdefiniować w drugim interfejsie, który ona będzie również dziedziczyć. Aby zrozumieć mechanizm obsługi kilku interfejsów prześledźmy poniższy przykład:

interface IMojInterfejs
{
    int Dodawanie();
    int Wynik
    {
        get;
        set;
    }
}
 
interface IOperacje
{
    int Mnozenie();
    int Dzielenie();
}
 
public class MojaKlasa : IMojInterfejs, IOperacje
{
    int a, b;
    //przechowuje wartosc wlasciwosci
    private int _wynik = 0;
 
    //konstruktor klasy MojaKlasa
    public MojaKlasa(int a, int b)
    {
        this.a = a;
        this.b = b;
    }
 
    //implementacja metody Dodawanie() z interfejsu IMojInterfejs
    public int Dodawanie()
    {
        return a + b;
    }
 
    //implementacja wlasciwosci z interfejsu IMojInterfejs
    public int Wynik
    {
        get { return _wynik; }
        set { _wynik = value; }
    }
 
    //implementacja metod z interfejsu IOperacje
    public int Mnozenie()
    {
        return a * b;
    }
 
    public int Dzielenie()
    {
        return a / b;
    }
}
 
public class Glowna
{
    static void Main()
    {
        MojaKlasa mk = new MojaKlasa(3, 7);
        System.Console.WriteLine("Wynik dodwania wynosi: {0}", mk.Dodawanie());
        mk.Wynik = mk.Mnozenie();
        System.Console.WriteLine("Wynik mnożenia wynosi: {0}", mk.Wynik);
 
        mk = new MojaKlasa(120, 3);
        System.Console.WriteLine("Wynik dzielenia wynosi: {0}", mk.Dzielenie());
    }
}

MojaKlasa dziedziczy teraz 2 interfejsy, co oznacza,  że musi udostępnić funkcjonalność metod oraz właściwości obu naraz (IMojInterfejs oraz IOperacje). Jak widzimy, aby dana klasa obsługiwała więcej niż jeden interfejs musimy na jej liście bazowej umieścić ich nazwy.

W wyniku powyższego przykładu otrzymamy:

Interfejsy możemy również rozszerzać. Innymi słowy, do istniejącego już interfejsu możemy dodawać nowe metody, czy właściwości lub istniejące zmieniać. Na przykład, możemy istniejący interfejs IOperacje rozszerzyć o nowy interfejs, w którym będzie opisana metoda obliczająca kwadrat z sumy dwóch liczba całkowitych. Taki mechanizm prezentuje poniższy przykład:

interface IMojInterfejs
{
    int Dodawanie();
    int Wynik
    {
        get;
        set;
    }
}
 
interface IKwadrat
{
    int KwadratSumy();
}
 
interface IOperacje : IKwadrat
{
    int Mnozenie();
    int Dzielenie();
}
 
public class MojaKlasa : IMojInterfejs, IOperacje
{
    int a, b;
    //przechowuje wartosc wlasciwosci
    private int _wynik = 0;
 
    //konstruktor klasy MojaKlasa
    public MojaKlasa(int a, int b)
    {
        this.a = a;
        this.b = b;
    }
 
    //implementacja metody Dodawanie() z interfejsu IMojInterfejs
    public int Dodawanie()
    {
        return a + b;
    }
 
    //implementacja wlasciwosci z interfejsu IMojInterfejs
    public int Wynik
    {
        get { return _wynik; }
        set { _wynik = value; }
    }
 
    //implementacja metod z interfejsu IOperacje
    public int Mnozenie()
    {
        return a * b;
    }
 
    public int Dzielenie()
    {
        return a / b;
    }
 
    //implementacja metody z interfejsu IKwadrat, obslugiwany przez interfejs IOperacje
    public int KwadratSumy()
    {
        return (a + b)*(a + b);
    }
}
 
public class Glowna
{
    static void Main()
    {
        MojaKlasa mk = new MojaKlasa(2, 8);
        System.Console.WriteLine("Wynik dodwania wynosi: {0}", mk.Dodawanie());
        mk.Wynik = mk.Mnozenie();
        System.Console.WriteLine("Wynik mnożenia wynosi: {0}", mk.Wynik);
        mk = new MojaKlasa(6, 3);
        System.Console.WriteLine("Wynik dzielenia wynosi: {0}", mk.Dzielenie());
        mk.Wynik = mk.KwadratSumy();
        System.Console.WriteLine("Kwadrat z sumy liczb: 6 i 3 wynosi: {0}", mk.Wynik);
    }
}

W powyższym przykładzie rozszerzyliśmy interfejs IOperacje o nowy interfejs IKwadrat, który udostępnia nową metodę obliczającą kwadrat z sumy dwóch liczb całkowitych (KwadratSumy()). MojaKlasa obsługuje interfejs IOperacje, dlatego też musieliśmy w niej zaimplementować metody obu interfejsów.

Po skompilowaniu i uruchomieniu tego przykładu otrzymamy następujące wyniki:

Tematem niniejszego artykułu były interfejsy. Pokazaliśmy sobie jak je zdefiniować oraz zaimplementować. Opowiedzieliśmy sobie również o różnicach między nimi a klasami abstrakcyjnymi. Wreszcie obsłużyliśmy kilka interfejsów oraz nauczyliśmy się sposobu ich rozszerzania.

Za tydzień nadal będziemy brnąć w świat interfejsów. Między innymi przybliżymy sobie sposoby dostępu do ich metod a także na podstawie przykładu przesłonimy zaimplementowany interfejs.


Spodobał Ci się ten artykuł? Podziel się z innymi!

Źródło:

Polecamy również w kategorii Kurs C#, cz. II