Podstawy programowania obiektowego #3 – Interfejsy

Pamiętasz, jak w poprzednim artykule wspominałem, że w Javie (jak i w wielu innych językach) dziedziczenie po wielu klasach z wielu względów nie jest dozwolone? Z pomocą przychodzą interfejsy, czyli specyficzna forma abstrakcji.

Problemy wielokrotnego dziedziczenia

Podstawową wadą dziedziczenia z wielu klas jest niejednoznaczność semantyczna takiego mechanizmu. Szczególnie opisuje to tzw. problem z diamentem (ang. diamond problem). Pojawia się on w przypadku kiedy mamy pewną klasę bazową, następnie z niej dziedziczą 2 podklasy, a poziom niżej mamy kolejną klasę wielokrotnie dziedziczącą z tych dwóch. Przyjrzyj się przykładowi poniżej.

Klasy Kontakt i Adres dziedziczą po klasie Osoba pola imie i nazwisko. Klasa Wizytowka dziedzicząc z nich obu odziedziczyłaby je więc… dwukrotnie, efektywnie nie wiedząc o co w ogóle programiście chodzi. W C++ taka sama sytuacja wystąpiła by również m.in. z konstruktorem i destruktorem, które wywoływałyby się dwukrotnie (trzeba również pamiętać o kolejności łańcuchowego wywoływania konstruktorów) – pomaga wykorzystanie dziedziczenia wirtualnego, ale nie o tym ten tekst. Patrząc na „klasyczne” dziedziczenie – komputer nie wie co ma zrobić z tymi niejednoznacznymi danymi.

Na ratunek interfejsy

Wspomniane paradoksy dotyczą jednak tylko implementacji – mając jedynie nie zaimplementowane metody i nie przejmując się atrybutami nie byłoby żadnego problemu. Takie też są interfejsy, które są strukturą która definiuje jedynie „co potrafi zrobić” dany obiekt, a nie „jak to robi”. Dla ludzi wywodzących się z C++ – interfejs zawiera jedynie metody czysto wirtualne. Nie przechowuje żadnych danych, ewentualnie jakieś stałe. Tak naprawdę jedyne, co powinien posiadać to szkielety metod – czyli ich nazwy i przyjmowane parametry. Kiedy klasa definiuje wszystkie metody danego interfejsu, to mówimy, że implementuje interfejs.

W ten sposób interfejsy pozbywają się ograniczeń klas abstrakcyjnych, a klasy mogą implementować tyle interfejsów ile tylko zechce programista. Na przykład możemy mieć interfejs Readable zawierający metodę read() (przepraszam za zmianę koncepcji nazewnictwa, ale nazwy interfejsów po polsku nie miałyby sensu – patrz odczytywalny, zapisywalny) i Writable z metodą write(). Wtedy np. klasa Book może implementować interfejs Readable, a już klasa Notebook (jako zeszyt) oba interfejsy Readable oraz Writable.

interface Readable{
    public String read();
}

interface Writable{
    public void write(String content);
}

public class Book implements Readable{
    private final String content ="Treść książki.";
    
    @Override
    public String read() {
        return this.content;
    }
}

public class Notebook implements Readable, Writable{
    private String content;
    
    @Override
    public String read() {
        return content;
    }

    @Override
    public void write(String content) {
        this.content += content;
    }
}

Zahaczając o polimorfizm, o którym w kolejnych artykułach, możemy przechowywać obiekt klasy Notebook pod maską interfejsu Readable, i wtedy odczytując nie będziemy nawet musieli zwracać uwagi czy to jest zeszyt, czy książka.

Readable readable = new Notebook();
readable.read();

W taki sam sposób możemy zresztą operować na kolekcjach w Javie – wszystkie implementują interfejs java.util.Collection, a na przykład różne rodzaje list (ArrayList, LinkedList, itp.) implementują java.util.List : List pages = new ArrayList<String>();

Podsumowując

  • Klasy abstrakcyjne nie pozwalają na dziedziczenie z wielu klas, za względu głównie na niejednoznaczność.
  • Interfejsy to konstrukcje udostępniające tylko operacje możliwe do wykonania, a pomijające dane oraz konkretne implementacje operacji.
  • Klasa może implementować dowolną liczbę interfejsów, ale musi implementować wszystkie ich metody.

 

Poprzedni artykuł: Podstawy programowania obiektowego #2  Abstrakcja i dziedziczenie w programowaniu obiektowym

Zobacz więcej (spis treści): Kurs programowania obiektowego #0: Wstęp i spis treści

Podsumowanie
Tytuł artykułu
Interfejsy w programowaniu obiektowym
Opis
Czym różnią się interfejsy od klas abstrakcyjnych? Dlaczego klasa może implementować wiele interfejsów w przeciwieństwie do dziedziczenia po wielu klasach? Czym jest problem z diamentem?
Autor

Leave a Comment

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *