Interfejs i dziedziczenie: najlepsze z obu światów?

10

„Odkryłem” interfejsy i zacząłem je kochać. Piękno interfejsu polega na tym, że jest to umowa, a każdy obiekt spełniający tę umowę może być używany wszędzie tam, gdzie wymagany jest interfejs.

Problem z interfejsem polega na tym, że nie może on mieć domyślnej implementacji, co utrudnia przyziemne właściwości i pokonuje DRY. Jest to również dobre, ponieważ utrzymuje implementację i system odłączony. Dziedziczenie, z drugiej strony, utrzymuje ściślejsze połączenie i może potencjalnie zerwać hermetyzację.

Przypadek 1 (Dziedziczenie z członkami prywatnymi, dobre enkapsulacja, ściśle powiązane)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

Przypadek 2 (tylko interfejs)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

Przypadek 3: (najlepszy z obu światów?)

Podobnie jak w przypadku 1 . Wyobraź sobie jednak, że (hipotetycznie) C ++ nie zezwalał na metody zastępujące, z wyjątkiem metod czysto wirtualnych .

Tak więc w przypadku 1 zastąpienie do_work () spowodowałoby błąd czasu kompilacji. Aby to naprawić, ustawiamy do_work () jako czystą wirtualną i dodajemy osobną metodę increment_money_earned (). Jako przykład:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

Ale nawet to ma problemy. Co jeśli za 3 miesiące Joe Coder utworzy Doktora, ale zapomni o wywołaniu increment_money_earned () w do_work ()?


Pytanie:

  • Czy przypadek 3 jest lepszy od przypadku 1 ? Czy to dlatego, że jest to „lepsza enkapsulacja” lub „luźniej sprzężona”, czy z innego powodu?

  • Czy przypadek 3 jest lepszy od przypadku 2, ponieważ jest zgodny z DRY?

MustafaM
źródło
2
... czy wymyślasz na nowo klasy abstrakcyjne czy co?
ZJR

Odpowiedzi:

10

Jednym ze sposobów rozwiązania problemu zapomnienia o wywołaniu superklasy jest przywrócenie kontroli nad superklasą! Zmieniłem twój pierwszy przykład, aby pokazać, jak (i ​​zmusiłem go do kompilacji;)). Och, ja też zakładać, że do_work()w Employeemiało być virtualw pierwszym przykładzie.

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

Teraz do_work()nie można go zastąpić. Jeśli chcesz go przedłużyć, musisz to zrobić, za pomocą on_do_work()którego do_work()ma on kontrolę.

Można tego oczywiście użyć z interfejsem z drugiego przykładu, jeśli Employeego rozszerzysz. Tak więc, jeśli dobrze cię rozumiem, myślę, że to czyni ten przypadek 3, ale bez konieczności używania hipotetycznego C ++! Jest SUCHY i ma silne zamknięcie.

Gyan alias Gary Buyn
źródło
3
I to jest wzorzec projektowy znany jako „metoda szablonu” ( en.wikipedia.org/wiki/Template_method_pattern ).
Joris Timmermans,
Tak, jest to zgodne z przypadkiem 3. To wygląda obiecująco. Zbadam szczegółowo. Jest to także pewien rodzaj systemu zdarzeń. Czy istnieje nazwa tego „wzoru”?
MustafaM,
@MadKeithV czy na pewno jest to „metoda szablonu”?
MustafaM,
@illmath - tak, jest to niewirtualna metoda publiczna, która przekazuje części szczegółów implementacji wirtualnym metodom chronionym / prywatnym.
Joris Timmermans
@illmath Nie myślałem o tym wcześniej jako o metodzie szablonowej, ale uważam, że jest to podstawowy przykład. Właśnie znalazłem ten artykuł, który możesz przeczytać tam, gdzie autor uważa, że ​​zasługuje na swoją własną nazwę: Idiom interfejsu nie-wirtualnego
Gyan alias Gary Buyn
1

Problem z interfejsem polega na tym, że nie może on mieć domyślnej implementacji, co utrudnia przyziemne właściwości i pokonuje DRY.

Moim zdaniem interfejsy powinny mieć tylko czyste metody - bez domyślnej implementacji. Nie narusza to w żaden sposób zasady DRY, ponieważ interfejsy pokazują, jak uzyskać dostęp do jakiegoś elementu. Tylko w celach informacyjnych szukam tutaj SUCHEGO wyjaśnienia :
„Każda wiedza musi mieć jedną, jednoznaczną, autorytatywną reprezentację w systemie”.

Z drugiej strony SOLID mówi ci, że każda klasa powinna mieć interfejs.

Czy przypadek 3 jest lepszy od przypadku 1? Czy to dlatego, że jest to „lepsza enkapsulacja” lub „luźniej sprzężona”, czy z innego powodu?

Nie, przypadek 3 nie jest lepszy od przypadku 1. Musisz podjąć decyzję. Jeśli chcesz mieć domyślną implementację, zrób to. Jeśli chcesz zastosować czystą metodę, skorzystaj z niej.

Co jeśli za 3 miesiące Joe Coder utworzy Doktora, ale zapomni o wywołaniu increment_money_earned () w do_work ()?

Następnie Joe Coder powinien zdobyć to, na co zasłużył, ignorując nieudane testy jednostkowe. Testował tę klasę, prawda? :)

Który przypadek jest najlepszy dla projektu oprogramowania, który może zawierać 40 000 wierszy kodu?

Jeden rozmiar nie pasuje do wszystkich. Nie można powiedzieć, który jest lepszy. Istnieją przypadki, w których jeden pasowałby lepiej niż drugi.

Może powinieneś nauczyć się wzorców projektowych zamiast próbować wymyślić własne.


Właśnie zdałem sobie sprawę, że szukasz wzoru wirtualnego interfejsu , ponieważ tak właśnie wygląda Twoja klasa 3.

BЈовић
źródło
Dziękuję za komentarz. Zaktualizowałem przypadek 3, aby wyjaśnić moje zamiary.
MustafaM,
1
Będę musiał cię tutaj -1. Nie ma żadnego powodu, aby twierdzić, że wszystkie interfejsy powinny być czyste lub że wszystkie klasy powinny dziedziczyć po interfejsie.
DeadMG
@DeadMG ISP
BЈовић
@VJovic: Istnieje duża różnica między SOLID a „Wszystko musi dziedziczyć z interfejsu”.
DeadMG
„Jeden rozmiar nie pasuje do wszystkich” i „poznaj niektóre wzorce projektowe” są poprawne - reszta odpowiedzi narusza Twoją własną sugestię, że jeden rozmiar nie pasuje do wszystkich.
Joris Timmermans
0

Interfejsy mogą mieć domyślne implementacje w C ++. Nic nie mówi, że domyślna implementacja funkcji nie zależy wyłącznie od innych wirtualnych elementów (i argumentów), więc nie zwiększa żadnego rodzaju sprzężenia.

W przypadku 2 DRY zastępuje się tutaj. Istnieje hermetyzacja, aby chronić Twój program przed zmianami, przed różnymi implementacjami, ale w tym przypadku nie masz różnych implementacji. Więc enkapsulacja YAGNI.

W rzeczywistości interfejsy czasu wykonywania są zwykle uważane za gorsze od ich odpowiedników w czasie kompilacji. W przypadku kompilacji możesz mieć zarówno przypadek 1, jak i przypadek 2 w tym samym pakiecie - nie wspominając już o wielu innych zaletach. Lub nawet w czasie wykonywania możesz po prostu zrobić Employee : public IEmployeetę samą przewagę. Istnieje wiele sposobów radzenia sobie z takimi rzeczami.

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

Przestałem czytać. YAGNI. C ++ jest tym, czym jest C ++, a Komitet ds. Standardów nigdy nie wdroży takiej zmiany z doskonałych powodów.

DeadMG
źródło
Mówisz „nie masz różnych implementacji”. Ale ja tak. Mam wdrożenie Nurse pracownika, a później mogę mieć inne implementacje (lekarz, woźny itp.). Zaktualizowałem przypadek 3, aby wyjaśnić, o co mi chodzi.
MustafaM
@illmath: Ale nie masz innych implementacji get_name. Wszystkie proponowane implementacje miałyby tę samą implementację get_name. Poza tym, jak powiedziałem, nie ma powodu, aby wybierać, możesz mieć oba. Ponadto przypadek 3 jest całkowicie bezwartościowy. Możesz zastąpić nieczyste wirtuale, więc zapomnij o projekcie, w którym nie możesz.
DeadMG
Interfejsy nie tylko mogą mieć domyślne implementacje w C ++, ale mogą też mieć domyślne implementacje i nadal być abstrakcyjne! tj. virtual void IMethod () = 0 {std :: cout << „Ni!” << std :: endl; }
Joris Timmermans,
@MadKeithV: Nie wierzę, że można je zdefiniować w linii, ale kwestia pozostaje taka sama.
DeadMG
@MadKeith: Jak gdyby Visual Studio kiedykolwiek było szczególnie dokładną reprezentacją Standard C ++.
DeadMG
0

Czy przypadek 3 jest lepszy od przypadku 1? Czy to dlatego, że jest to „lepsza enkapsulacja” lub „luźniej sprzężona”, czy z innego powodu?

Z tego, co widzę w twojej implementacji, twoja implementacja Case 3 wymaga klasy abstrakcyjnej, która może implementować czysto wirtualne metody, które można później zmienić w klasie pochodnej. Przypadek 3 byłby lepszy, ponieważ klasa pochodna może zmieniać implementację do_work, gdy jest to wymagane, a wszystkie pochodne instancje zasadniczo należą do podstawowego typu abstrakcyjnego.

Który przypadek jest najlepszy dla projektu oprogramowania, który może zawierać 40 000 wierszy kodu.

Powiedziałbym, że zależy to wyłącznie od projektu wdrożenia i celu, który chcesz osiągnąć. Klasa abstrakcyjna i interfejsy są implementowane w oparciu o problem, który należy rozwiązać.

Edytuj na pytanie

Co jeśli za 3 miesiące Joe Coder utworzy Doktora, ale zapomni o wywołaniu increment_money_earned () w do_work ()?

Testy jednostkowe można wykonać, aby sprawdzić, czy każda klasa potwierdza oczekiwane zachowanie. Więc jeśli zastosowane zostaną odpowiednie testy jednostkowe, można uniknąć błędów, gdy Joe Coder zaimplementuje nową klasę.

Karthik Sreenivasan
źródło
0

Korzystanie z interfejsów powoduje uszkodzenie DRY tylko wtedy, gdy każda implementacja jest duplikatem każdej innej. Możesz rozwiązać ten dylemat, stosując zarówno interfejs, jak i dziedziczenie, ale w niektórych przypadkach możesz chcieć zaimplementować ten sam interfejs na kilku klasach, ale zmieniać zachowanie w każdej z klas, a to nadal będzie zgodne z zasadą SUCHEGO. To, czy zdecydujesz się na użycie jednego z 3 opisanych przez ciebie podejść, sprowadza się do wyborów, które musisz podjąć, aby zastosować najlepszą technikę, by dopasować się do danej sytuacji. Z drugiej strony prawdopodobnie z czasem zauważysz, że korzystasz z interfejsów częściej i dziedziczysz tylko tam, gdzie chcesz usunąć powtórzenie. To nie znaczy, że to jedyne powód dziedziczenia, ale lepiej jest zminimalizować użycie dziedziczenia, aby umożliwić pozostawienie otwartych opcji, jeśli okaże się, że projekt musi się zmienić później, a jeśli chcesz zminimalizować wpływ na klasy potomne z efektów, które zmiana wprowadziłby w klasie nadrzędnej.

S.Robins
źródło