Jaki sens mają interfejsy?

1

Ja zauważyłem sprawę w C# i Java, ale pytanie może dotyczyć każdego języka z klasami i interfejsami. Nie chodzi mi o szkolne projekciki, których głównym celem jest zapoznanie się i przetestowanie pewnych elementów języka. Chodzi mi o to, czy w praktycznym programowaniu interfejsy mają sens.

ZTCW, interfejs to jaki byt podobny do klasy, który zawiera tylko nazwy i parametry metod, a każda klasa implementująca dany interfejs musi zawierać implementacje metod wymienionych w interfejsie.

Ja nieraz, jak coś programowałem, to miałem wizję dwóch wariantów danego obiektu, więc tworzyłem interfejs zawierający zapis metod, gdyż same metody były odmienne w poszczególnych wariantach. Jednakże, prędzej czy później jakiś kawałek kodu, funkcja wykorzystywana w metodach była podobna, a nieraz taka sama w co najmniej dwóch wariantach. Aby nie dublować kodu, byłem zmuszony zaimplementować funkcję w interfejsie, a interfejs przerobić na klasę. W tym momencie dwie klasy implementujące jeden interfejs zmieniają się na dwie klasy dziedziczące po jednej klasie. Jednak z miejscach wykorzystywania interfejsu i klas nie musiałem robić samych zmian.

W drugą stronę patrząc, jeżeli interfejs w danym przypadku miałbym zastosowanie, to moim zdaniem nie ma żadnej różnicy, czy zaimplementuje się interfejs, czy klasę abstrakcyjną z metodami abstrakcyjnymi, ponieważ:

  1. W jednym i drugim przypadku klasa pochodna musi implementować metody klasy nadrzędnej lub interfejsu.
  2. Pole typu klasy nadrzędnej lub interfejsu raz może być obiektem jednej, a raz może być obiektem innej klasy podrzędnej.
  3. Nie można utworzyć obiektu ani typu interfejsu ani typu klasy abstrakcyjnej.

Wobec powyższego wychodzi na to, że klasa abstrakcyjna i interfejs to jest to samo z tą różnicą, że klasa abstrakcyjna może mieć w sobie jakieś pola lub metody, które są wspólne dla klas wykorzystujących interfejs.

Czy interfejsy mają faktycznie jakiś sens i jaki sens? Czy to jest tylko zaszłość historyczna lub mało użyteczny bajer?

Skoro klasa abstrakcyjna ma cechy podobne do interfejsu a ma większe możliwości, to po co wymyślono interfejsy?

Przykład pierwszy z brzegu: link

interface IEquatable<T>
{
    bool Equals(T obj);
}

public class Car : IEquatable<Car>
{
    public string? Make { get; set; }
    public string? Model { get; set; }
    public string? Year { get; set; }

    // Implementation of IEquatable<T> interface
    public bool Equals(Car? car)
    {
        return (this.Make, this.Model, this.Year) ==
            (car?.Make, car?.Model, car?.Year);
    }
}

Może być też tak:

abstract class IEquatable<T>
{
    abstract bool Equals(T obj);
}

public class Car : IEquatable<Car>
{
    public string? Make { get; set; }
    public string? Model { get; set; }
    public string? Year { get; set; }

    // Implementation of IEquatable<T> interface
    public override bool Equals(Car? car)
    {
        return (this.Make, this.Model, this.Year) ==
            (car?.Make, car?.Model, car?.Year);
    }
}

To jest to samo, prawda?

11

Wiele języków, jak Java czy C# nie wspiera wielodziedziczenia, więc podział na klasy abstrakcyjne i interfejsy ma sens (bo mozna implementować wiele interfejsów i to nie egzotyka). Ale w ogólności to masz rację, klasa abstrakcyjna, która ma tylko metody abstrakcyjne jest tożsama z interfejsem i tak to można robić w językach, które nie mają explicite interfejsów, jak C++.

10

Aby nie dublować kodu, byłem zmuszony zaimplementować funkcję w interfejsie, a interfejs przerobić na klasę. W tym momencie dwie klasy implementujące jeden interfejs zmieniają się na dwie klasy dziedziczące po jednej klasie.

Dlatego warto stosować kompozycję zamiast dziedziczenia. Jeśli jakaś logikę można uwspólnić, to lepiej wynieść do osobnej klasy i korzystać z niej z różnych miejsc.

2

Najprostsze wyjaśnienie - w Java i C# możesz dziedziczyć po jednej klasie abstrakcyjnej, ale możesz zadeklarować, że implementujesz wiele interfaceów.
Drugi powód jest taki, ze dziedziczenie ma w sobie trochę niebezpieczeństw wynikających z tego, że nie wiesz co się dzieje w bebechach klasy nadrzędnej. W tym ujęciu interface ma tę przewagę, że wiesz, że nikt nic tam za bardzo nie namieszał.

0
Charles_Ray napisał(a):

Aby nie dublować kodu, byłem zmuszony zaimplementować funkcję w interfejsie, a interfejs przerobić na klasę. W tym momencie dwie klasy implementujące jeden interfejs zmieniają się na dwie klasy dziedziczące po jednej klasie.

Dlatego warto stosować kompozycję zamiast dziedziczenia. Jeśli jakaś logikę można uwspólnić, to lepiej wynieść do osobnej klasy i korzystać z niej z różnych miejsc.

Trochę naciągany i trochę bezsensowny przykład, ale spróbuję nakreślić o co mi chodzi.

Załóżmy, że pierwotny pomysł jest taki:

interface Num
{
    int Proc();
}

class NumAdd : Num
{
    int ReadNum()
    {
        string S = Console.ReadLine();
        try
        {
            return int.Parse(S);
        }
        catch
        {
            return 0;
        }
    }

    int Proc()
    {
        int N1 = ReadNum();
        int N2 = ReadNum();
        return N1 + N2;
    }
}

class NumMul : Num
{
    int ReadNum()
    {
        string S = Console.ReadLine();
        try
        {
            return int.Parse(S);
        }
        catch
        {
            return 0;
        }
    }

    int Proc()
    {
        int N1 = ReadNum();
        int N2 = ReadNum();
        return N1 * N2;
    }
}

Jak widać, powtarza się funkcja ReadNum, wiec interfejs muszę zamienić na klasę:

abstract class Num
{
    virtual int Proc()
    {
        return 0;
    }

    int ReadNum()
    {
        string S = Console.ReadLine();
        try
        {
            return int.Parse(S);
        }
        catch
        {
            return 0;
        }
    }
}

class NumAdd : Num
{
    override int Proc()
    {
        int N1 = ReadNum();
        int N2 = ReadNum();
        return N1 + N2;
    }
}

class NumMul : Num
{
    override int Proc()
    {
        int N1 = ReadNum();
        int N2 = ReadNum();
        return N1 * N2;
    }
}

Czy dobrze rozumiem to, co proponuje Charles_Ray?

interface Num
{
    int Proc();
}

class NumTools
{
    static int ReadNum()
    {
        string S = Console.ReadLine();
        try
        {
            return int.Parse(S);
        }
        catch
        {
            return 0;
        }
    }
}

class NumAdd : Num
{
    int Proc()
    {
        int N1 = NumTools.ReadNum();
        int N2 = NumTools.ReadNum();
        return N1 + N2;
    }
}

class NumMul : Num
{
    int Proc()
    {
        int N1 = NumTools.ReadNum();
        int N2 = NumTools.ReadNum();
        return N1 * N2;
    }
}

Tak swoją drogą, to trochę nie zrozumiały jest sens słów "virtual" i "override". Co stoi na przeszkodzie, żeby, jak kompilator stwierdzi, że w klasie bazowej i pochodnej jest metoda o tej samej nazwie i tych samych parametrach, to żeby w użyciu była metoda z klasy pochodnej. Cała idea dziedziczenia polega na tym, ze zaimplementowana metoda klasy pochodnej zastępuje metodę klasy bazowej. Kiedyś próbowałem i bez słowa "virtual" i "overrided", metoda klasy pochodnej nie zastępowała metody klasy bazowej pomimo, że program kompilował się.

1

Parę problemów w jednym poście.

Inreface i klasy to nie jest coś ściśle ze sobą związanego. Ogólnie implementacja czegokolwiek w interface to jakiś dirtyhack, mający zastosowanie chyba jedynie przy refaktoringu, jak musimy zmienić interface, a nie chcemy zmieniać implementacji w klasach, które deklarują implementację - np. masz bibliotekę do obsługi kolekcji i interface list:

interface List{
  add(Item item)
}

Biblioteka została wykorzystana w iluś tam projektach i chcesz dodać kolejną metodę addAll. Jeżeli ją dodasz, to projekty nie będą się kompilować, bo nie implementują tej metody. robisz więc coś takiego:

interface List{
  add(Item item)
  addAll(List list){
    for....
      this.add(item)
  }
}

Ogólnie interface określa zbiór dostępnych metod, a klasa opisuje w jaki sposób te metody mają działać.

Inny przykład tworzysz sobie jakiś komponent wizualny, wyświetlający na ekranie tabelkę, na podstawie jakiegoś modelu. Model opisujesz interfacem:

interface ViewSupplier{
  TableCell(int line)
}

Dzięki temu, możesz sobie zrobić klasę z taką deklaracją:

ListViewAdapter implements ViewSupplier, List{
....
}

Którą jednocześnie możesz traktować jak kolekcję i jak źródło komórek dla tabelki.

Popatrz sobie na wzorce projektowe, ale na spokojnie, spróbuj zrozumieć o co w nich chodzi, kiedy ich użyć i jaką rolę pełni interface w takim dla przykładu Proxy, albo Dekoratorze.

7
andrzejlisek napisał(a):

W tym momencie dwie klasy implementujące jeden interfejs zmieniają się na dwie klasy dziedziczące po jednej klasie. Jednak z miejscach wykorzystywania interfejsu i klas nie musiałem robić samych zmian.>

Tak, czasami może się tak zdarzyć.

Skoro klasa abstrakcyjna ma cechy podobne do interfejsu a ma większe możliwości, to po co wymyślono interfejsy?

Po to, aby można było nie używać klas abstrakcyjnych tam, gdzie one nie mają sensu.
Patrzysz na kod z punktu widzenia, w którym jesteś jego wyłącznym autorem, tymczasem często tak nie jest. Interfejsy są (między innymi) po to, abyś mógł wymóc na innym kodzie kształt, którego Ty potrzebujesz w swoim kodzie. Np. napisałeś program do edycji grafiki, i chciałbyś dać możliwość eksportu grafiki do różnych formatów, które niekoniecznie w danym momencie chcesz sam implementować.

0

Patrzysz na kod z punktu widzenia, w którym jesteś jego wyłącznym autorem, tymczasem często tak nie jest. Interfejsy są (między innymi) po to, abyś mógł wymóc na innym kodzie kształt, którego Ty potrzebujesz w swoim kodzie.

Tak, to prawda, wszystkie projekty hobbystycznie tworzę samodzielnie i w czasie ich implementacji wyszedł problem będący przedmiotem tego wątku. W pracy zawodowej nie spotkałem się z takim problemem.

Np. napisałeś program do edycji grafiki, i chciałbyś dać możliwość eksportu grafiki do różnych formatów, które niekoniecznie w danym momencie chcesz sam implementować.

Czy dobrze rozumiem, że to powinno być tak, że:
Mam przygotowany interfejs zawierający metody zapisu i odczytu grafiki z pliku, nazywam go ImgFile. Daję innemu programiście zadanie zaimplementowania klasy do zapisu PNG i JPG. Programista w pierwszej chwili zrobi klasy:

interface ImgFile
{
}
class ImgFilePng : ImgFile
{
}
class ImgFileJpg : ImgFile
{
}

Potem programista stwierdzi, że w obu formatach są pewne elementy wspólne. To wtedy powinien by napisać coś takiego:

interface ImgFile
{
}
class ImgFileCommon : ImgFile
{
    // Tutaj pola i metody używane w obu formatach grafik
}
class ImgFilePng : ImgFileCommon
{
}
class ImgFileJpg : ImgFileCommon
{
}

Natomiast ja mam cały czas interfejs ImgFile, którego nikt nie modyfikuje i programista od formatów musi tak zaimplementować funkcjonalnośc, żeby była użyteczna bez modyfikacji interfejsu. Czy to właśnie taka jest idea interfejsu?

0
andrzejlisek napisał(a):

Potem programista stwierdzi, że w obu formatach są pewne elementy wspólne. To wtedy powinien by napisać coś takiego:

Ciężko powiedzieć, ale według mnie nie powinien. Co jak logika zawarta w ImgFileCommon przyda się gdzieś w zupełnie innym miejscu niezależnym od ImgFilePng lub ImgFileJpg. Twoja abstrakcja się sypie, najgorsze co można zrobić to dziedziczenie po takiej klasie dostając całą dżunglę zamiast banana: https://medium.com/codemonday/banana-gorilla-jungle-oop-5052b2e4d588 .

Interfejsy są lepsze niż dziedziczenie czy klasy abstrakcyjne (oczywiście według mnie, to bardzo dyskusyjne), bo jasno mówią co dany kawałek robi. Np. w twoim przypadku nie wiem co znaczy ImgFile. Czy zadaniem jest dekodowanie formatów do jakiegoś wspólnego np. bitmapy? Jak mam ImageDecoder i PngDecoder to same nazwy co mówią

2

@andrzejlisek: Moim zdaniem myśląc o interfaceach myślisz o implementacji i "jak to będzie działać". Załóżmy, że masz do zrobienia jakiś komponent, który ma wyświetlać obrazki. Nie ważne skąd, nieważne w jakim formacie. Definiujesz sobie interface, który określa minimalne warunki jakie musi spełniać porcja danych do wyświetlenia:

interface ImageSupplier{
  Bitmap getImage();
}

W tym momencie wiesz, że je coś, co zostanie ci dostarczone, będzie ok. Jednocześnie w żaden sposób nie narzucasz programiście, który będzie pisał klasę implementującą ten interface jak ma to zrobić. Jedyne warunki są definiowane przez powyższy interface.

Ktoś bierze się za pisanie obsługi plików i dekodowanie ich do postaci bitmapy (tu wg aktualnych trendów należałoby użyć enkapsulacji, ale chcę pokazać to na przykładzie klasy abstrakcyjnej)

abstract class FileImageSupplier implements ImageSupplier{
  public Bitmap getImage() {
    return decodeImage(readFile());
  }
  private byte[] readFile(){
    //nudy, nudy nudy
  }

  abstract protected Bitmap decodeImage(data byte[]);

}

Ktoś dostaje zadanie dopisania możliwości obsługi plików .png

class PngFileSupplier extends FileImageSupplier{
 @Override
 protected Bitmap decodeImage(data byte[]){
   //implementacja
 }
}

Tworzysz też następne klasy dla jpg, tiff, co tam jeszcze...

Zauważ, że każdą z tych rzeczy może tworzyć niezależny programista, bazując jedynie na wiedzy, że ma zaimplementować, bądź rozszerzyć konkretny interface / klasę. Żadna z tych klas nie musi wiedzieć za dużo o tym co jest nad nią i co jest pod nią.

Dla własnych przemyśleń - co się stanie jeżeli pojawią się dodatkowe wymagania, że "plik" może być też jakims blobem z bazy danych, blob storage, URLem.
Gdzie w tym wszystkim jest miejsce dla wzorców fabrykujących? Factory method, factory, abstract factory...

2
andrzejlisek napisał(a):

Czy dobrze rozumiem, że to powinno być tak, że:
Mam przygotowany interfejs zawierający metody zapisu i odczytu grafiki z pliku

A to w sumie inny temat, bo w większości przypadków lepiej byłoby takie interfejsy rozdzielić.

Natomiast ja mam cały czas interfejs ImgFile, którego nikt nie modyfikuje i programista od formatów musi tak zaimplementować funkcjonalnośc, żeby była użyteczna bez modyfikacji interfejsu. Czy to właśnie taka jest idea interfejsu?

Ogólnie tak, Ty wymagasz interfejsu, a to jak ktoś go zaimplementował, czy gdzieś tam jest jakiś wspólny kod, i czy został umieszczony w klasie bazowej, czy jakiejś innej, to nie ma dla Ciebie znaczenia.

2
andrzejlisek napisał(a):

Potem programista stwierdzi, że w obu formatach są pewne elementy wspólne. To wtedy powinien by napisać coś takiego:

```c#
class ImgFileCommon : ImgFile
{
    // Tutaj pola i metody używane w obu formatach grafik
}

Czy to nie jest przypadkiem ta klasa, która z czasem przybiera postać potworka nazwanego "Utils:, a każda metoda dostaje n-parametrów z czasem, i tam rosną sobie te smutne switche i if'y?

1

Tak na prawdę na pytanie "jaki interfejsy mają sens", nie da się jednoznacznie odpowiedzieć, bo odpowiedź zależy od tego z jakiej perspektywy zadajesz pytanie:

  1. Z punktu widzenia końcowego oprogramowania
  2. Z punktu widzenia zależności między różnymi elementami oprogramowania
  3. Z punktu widzenia wzorców projektowych
  4. Z punktu widzenia interfejsów w danym języku programowania
  5. Z punktu widzenia klas i użytkowników tego interfejsu.

I odpowiedzi na pytanie "Jaki sens ma interfejs", będzie brzmiało inaczej, zależnie od tego z jakiej strony do niego podejdziesz.

  1. Z punktu widzenia końcowego oprogramowania, to interfejs to jest tylko i wyłącznie szczegół implementacyjny, i nie ma żadnego znaczenia
  2. Z punktu widzenia zależności między różnymi elementami oprogramowania, to interfejsy to jest jeden ze sposobów używania Dependency Inversion i polimorfizmu pomiędzy różnymi elementami w aplikacji (zapewniających większą niezależność między jednym elementem oprogramowania i drugim)
  3. Z punktu widzenia wzorców projektowych, interfejs to jest element języka będący "językiem"/"medium komunikacyjnym"/"częścią wspólną" pomiędzy różnymi elementami tego wzorca (np fabryka abstrakcyjna i użytkownik tej fabryki)
  4. Z punktu widzenia interfejsów w danym języku programowania, np w javie, interfejsy to jest inny zapis klasy abstrakcyjnej (trochę łatwiejszy i pozwalający rozszerzać wiele interfejsów). Są w tym miejscu pewnym medium pomiędzy klasami użytkownika a biblioteką standardową języka.
  5. Z punktu widzenia klas i użytkowników klas, interfejs to jest po prostu typ prezentujący sygnatury bez implementacji, używany głównie do polimorfizmu, Open/Close i Dependency Inversion.

Częścią wspólną tych wszystkich punktów jest polimorfizm, ale też nie koniecznie, bo polimorfizm można też osiągnąć klasami abstrakcyjnymi lub po prostu metodami wirtualnymi.

Twoje pytanie (bo zapytałeś o IEquatable<T>) wpada w punkt 4., czyli użycie interfejsów w danym języku programowania, w tym przypadku C#.

2
andrzejlisek napisał(a):

Jednakże, prędzej czy później jakiś kawałek kodu, funkcja wykorzystywana w metodach była podobna, a nieraz taka sama w co najmniej dwóch wariantach. Aby nie dublować kodu, byłem zmuszony zaimplementować funkcję w interfejsie, a interfejs przerobić na klasę. W tym momencie dwie klasy implementujące jeden interfejs zmieniają się na dwie klasy dziedziczące po jednej klasie. Jednak z miejscach wykorzystywania interfejsu i klas nie musiałem robić samych zmian.

I tu jest błąd (subiektywnie), sytuacja się zmienia a ty chcesz za wszelką cenę pozostawić kod w bieżącej formie dodając pewnego rodzaju łatkę. Raz się uda, drugi raz też, za dziesiątym to już będzie walka ze smokami. IMO w takiej sytuacji trzeba zrobić redesing, dodać jakąś klasę pomiędzy tak, żeby wyabstrachować część zmienną od niezmiennej.

Co do pytania: jak w sumie każdy wysokopoziomowy ficzer: dostajesz jakiś ograniczony mechanizm, który zapewnia jakąś zaletę, bo ograniczenie coś wnosi. Przykładowo takie słowo kluczowe const z C/C++: sprawia, że nie możemy zmienić danej zmiennej. Niby to samo można uzyskać dobrą samokontrolą, ale fajnie mieć trochę pomocy od kompilatora + jest szansa, że optymalizator coś lepiej podziała.

Tak samo jest z interfejsem: przez to, że mniej może to czytając interfejs/implementację nie mam problemów w stylu czy ta klasa ma jakiś stan albo co klasa dziedziczona robi ze stanem. Dziedziczenie gotowych implementacji jest według mnie straszne jeśli chodzi o czytelność kodu (słynne fragile base class problem) a dziedziczenie pól to według mnie 100x gorszy problem, bo nie widzę zastosowania a mutujący stan pomiędzy różnymi poziomami dziedziczenia to koszmar. Do tego odpada problem kilku poziomów dziedziczenia (czyli kolejna komplikacja utrudniająca analizę kodu)

0
andrzejlisek napisał(a):

Czy dobrze rozumiem, że to powinno być tak, że:
Mam przygotowany interfejs zawierający metody zapisu i odczytu grafiki z pliku, nazywam go ImgFile. Daję innemu programiście zadanie zaimplementowania klasy do zapisu PNG i JPG. Programista w pierwszej chwili zrobi klasy:

To jest ogólnie słaby pomysł projektować interfejs pisząc go "od środka". Tzn tworzysz klasę, próbujesz ją zaimplementować, i implementując ustalasz interfejs.

Dużo lepszy podejściem jest znaleźć miejsce w kodzie gdzie chcesz jej użyć zanim powstanie, wtedy stworzyć interfejs, i dopiero później go zaimplementować.

0

Nie mają żadnego sensu. Tzn nie sama koncepcja nie ma sensu, tylko nie ma sensu odróżniać interfejsu jako takiego od klasy i dlatego np w Dart nie ma interfejsów, bo każda klasa może być interfejsem:
https://dart.dev/guides/language/language-tour#implicit-interfaces

Dotyczy to też klas abstrakcyjnych. Zgodnie z logiką (i nazwą), implements od extends różni się tylko tym, że to pierwsze wymaga override na wszytkich metodach klasy.

1 użytkowników online, w tym zalogowanych: 0, gości: 1