„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?
źródło
Odpowiedzi:
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()
wEmployee
miało byćvirtual
w pierwszym przykładzie.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óregodo_work()
ma on kontrolę.Można tego oczywiście użyć z interfejsem z drugiego przykładu, jeśli
Employee
go 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.źródło
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.
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.
Następnie Joe Coder powinien zdobyć to, na co zasłużył, ignorując nieudane testy jednostkowe. Testował tę klasę, prawda? :)
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.
źródło
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 IEmployee
tę samą przewagę. Istnieje wiele sposobów radzenia sobie z takimi rzeczami.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.
źródło
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.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.
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
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ę.
źródło
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.
źródło