Studiuję OOP w C ++ i chociaż jestem świadomy definicji tych 3 pojęć, tak naprawdę nie jestem w stanie zrozumieć, kiedy i jak go używać.
Użyjmy tej klasy na przykład:
class Person{
private:
string name;
int age;
public:
Person(string p1, int p2){this->name=p1; this->age=p2;}
~Person(){}
void set_name (string parameter){this->name=parameter;}
void set_age (int parameter){this->age=parameter;}
string get_name (){return this->name;}
int get_age (){return this->age;}
};
1. Singleton
JAK działa ograniczenie klasy, aby mieć tylko jeden obiekt?
CAN zaprojektować klasy, która miałaby TYLKO 2 instancje? A może 3?
KIEDY stosowanie singletonu jest zalecane / konieczne? Czy to dobra praktyka?
O ile wiem, jeśli jest tylko jedna funkcja wirtualna, klasa staje się abstrakcyjna. Więc dodając
virtual void print ()=0;
zrobiłby to, prawda?
DLACZEGO potrzebujesz klasy, której obiekt nie jest wymagany?
3. interfejs
Jeśli interfejs jest klasą abstrakcyjną, w której wszystkie metody są czysto wirtualnymi funkcjami, to
JAKA jest główna różnica między nimi dwoma?
Z góry dziękuję!
Odpowiedzi:
1. Singleton
Ograniczasz liczbę instancji, ponieważ konstruktor będzie prywatny, co oznacza, że tylko metody statyczne mogą tworzyć instancje tej klasy (istnieją inne brudne sztuczki, aby to osiągnąć, ale nie dajmy się zwieść).
Stworzenie klasy, która będzie miała tylko 2 lub 3 instancje, jest całkowicie wykonalne. Powinieneś używać singletona, ilekroć poczujesz konieczność posiadania tylko jednej instancji tej klasy w całym systemie. Zazwyczaj dzieje się tak w przypadku klas, które mają zachowanie „kierownika”.
Jeśli chcesz dowiedzieć się więcej o Singletonach, możesz zacząć w Wikipedii, a zwłaszcza w C ++ w tym poście .
Zdecydowanie jest kilka dobrych i złych rzeczy w tym schemacie, ale ta dyskusja należy gdzie indziej.
2. Klasy abstrakcyjne
Tak to prawda. Tylko jedna wirtualna metoda oznaczy klasę jako abstrakcyjną.
Będziesz używał tego rodzaju klas, jeśli masz większą hierarchię klas, w której najwyższe klasy nie powinny być tworzone.
Załóżmy, że definiujesz klasę ssaków, a następnie dziedziczysz ją dla psów i kotów. Jeśli się nad tym zastanowić, nie ma większego sensu, aby mieć czystą instancję Ssaka, ponieważ najpierw trzeba wiedzieć, co to naprawdę jest Ssak.
Potencjalnie istnieje metoda o nazwie MakeSound (), która będzie miała sens tylko w klasach odziedziczonych, ale nie ma wspólnego dźwięku, który mogą wytwarzać wszystkie ssaki (jest to tylko przykład, który nie próbuje tutaj uzasadnić dźwięków ssaków).
Oznacza to, że Ssak powinien być klasą abstrakcyjną, ponieważ będzie miał pewne wspólne zachowanie zaimplementowane u wszystkich ssaków, ale tak naprawdę nie powinien być tworzony. To podstawowa koncepcja klas abstrakcyjnych, ale zdecydowanie powinieneś się jej nauczyć.
3. Interfejsy
W C ++ nie ma czystych interfejsów w tym samym sensie, co w Javie lub C #. Jedynym sposobem na stworzenie takiego jest posiadanie czystej, abstrakcyjnej klasy, która naśladuje większość zachowań, których oczekujesz od interfejsu.
Zasadniczo zachowaniem, którego szukasz, jest zdefiniowanie umowy, z którą inne obiekty mogą wchodzić w interakcje bez dbania o implementację. Kiedy tworzysz klasę czysto abstrakcyjną, oznacza to, że cała implementacja należy gdzie indziej, więc celem tej klasy jest tylko kontrakt, który ona definiuje. Jest to bardzo potężna koncepcja w OO i zdecydowanie powinieneś się nią bardziej przyjrzeć.
Możesz przeczytać o specyfikacji interfejsu dla C # w MSDN, aby uzyskać lepszy pomysł:
http://msdn.microsoft.com/en-us/library/ms173156.aspx
C ++ zapewni takie samo zachowanie, mając czystą abstrakcyjną klasę.
źródło
Większość osób już wyjaśniła, czym są singletony / klasy abstrakcyjne. Mam nadzieję, że przedstawię nieco inną perspektywę i podam kilka praktycznych przykładów.
Singletony - jeśli chcesz, aby cały kod wywołujący używał jednej instancji zmiennych, niezależnie od przyczyn, masz następujące opcje:
Ze wszystkich złych i złych opcji, jeśli potrzebujesz globalnych danych, singleton jest O wiele lepszym podejściem niż którekolwiek z poprzednich dwóch. Pozwala także zachować otwarte opcje, jeśli jutro zmienisz zdanie i zdecydujesz się zastosować odwrócenie kontroli zamiast globalnych danych.
Więc gdzie użyłbyś singletona? Oto kilka przykładów:
Rejestrowanie - jeśli chcesz, aby cały proces miał jeden dziennik, możesz utworzyć obiekt dziennika i przekazywać go wszędzie. Ale co, jeśli masz 100 000 000 wierszy starszego kodu aplikacji? zmodyfikować je wszystkie? Możesz też po prostu wprowadzić następujące informacje i rozpocząć korzystanie z nich w dowolnym miejscu:
Pamięć podręczna połączenia z serwerem - to było coś, co musiałem przedstawić w naszej aplikacji. Nasza baza kodu, a było jej dużo, wykorzystywana do łączenia się z serwerami, kiedy tylko zechce. Przez większość czasu było to w porządku, chyba że w sieci występowało jakieś opóźnienie. Potrzebowaliśmy rozwiązania i przeprojektowanie 10-letniej aplikacji nie było tak naprawdę na stole. Napisałem singleton CServerConnectionManager. Następnie przeszukałem kod i zastąpiłem wywołania CoCreateInstanceWithAuth identycznym wywołaniem podpisu, które wywołało moją klasę. Teraz po pierwszej próbie połączenie zostało zapisane w pamięci podręcznej, a przez resztę czasu próby „połączenia” były natychmiastowe. Niektórzy twierdzą, że singletony są złe. Mówię, że uratowali mi tyłek.
Podczas debugowania często bardzo przydatna jest globalna tablica obiektów. Mamy kilka zajęć, które chcielibyśmy śledzić. Wszystkie pochodzą z tej samej klasy podstawowej. Podczas tworzenia instancji wywołują tablicę obiektów singleton i rejestrują się. Gdy zostaną zniszczone, wyrejestrowują się. Mogę podejść do dowolnej maszyny, dołączyć się do procesu i stworzyć listę działających obiektów. Byłem w produkcie od ponad pół dekady i nigdy nie czułem, że kiedykolwiek potrzebujemy 2 „globalnych” tabel obiektów.
Mamy pewne stosunkowo złożone klasy narzędzi do analizowania składni łańcuchów, które opierają się na wyrażeniach regularnych. Klasy wyrażeń regularnych muszą zostać zainicjowane, zanim będą mogły wykonywać dopasowania. Inicjalizacja jest nieco kosztowna, ponieważ wtedy FSM jest generowany na podstawie łańcucha analizy. Jednak po tym do klasy wyrażeń regularnych można bezpiecznie uzyskać dostęp przez 100 wątków, ponieważ po zbudowaniu FSM nigdy się nie zmienia. Te klasy parsera wewnętrznie używają singletonów, aby mieć pewność, że inicjalizacja nastąpi tylko raz. To znacznie poprawiło wydajność i nigdy nie spowodowało żadnych problemów z powodu „złych singletonów”.
Powiedziawszy to wszystko, musisz pamiętać, kiedy i gdzie stosować singletony. 9 na 10 razy jest lepsze rozwiązanie i zdecydowanie powinieneś go użyć. Są jednak chwile, kiedy singleton jest absolutnie właściwym wyborem projektowym.
Następny temat ... interfejsy i klasy abstrakcyjne. Po pierwsze, jak wspomniano inni, interfejs JEST klasą abstrakcyjną, ale wykracza poza to, wymuszając, że nie ma absolutnie żadnej implementacji. W niektórych językach słowo kluczowe interfejsu jest częścią języka. W C ++ używamy po prostu klas abstrakcyjnych. Microsoft VC ++ zrobił jeden krok, aby zdefiniować to gdzieś wewnętrznie:
... więc nadal możesz używać słowa kluczowego interfejsu (będzie nawet podświetlone jako „prawdziwe” słowo kluczowe), ale jeśli chodzi o rzeczywisty kompilator, jest to po prostu struct.
Gdzie byś tego użył? Wróćmy do mojego przykładu działającej tabeli obiektów. Powiedzmy, że klasa podstawowa ma ...
virtual void print () = 0;
Oto twoja klasa abstrakcyjna. Wszystkie klasy, które używają tabeli obiektów wykonawczych, będą pochodzić z tej samej klasy podstawowej. Klasa podstawowa zawiera wspólny kod do rejestracji / wyrejestrowania. Ale sam nigdy nie zostanie utworzony. Teraz mogę mieć klasy wyprowadzające (np. Żądania, detektory, obiekty połączenia klienta ...), każdy zaimplementuje print (), aby po dołączeniu do procesu i zapytaniu go, co się dzieje, każdy obiekt zgłosi swój własny stan.
Przykłady abstrakcyjnych klas / interfejsów są niezliczone i zdecydowanie używasz ich (lub powinieneś używać) o wiele, dużo częściej niż singletonów. Krótko mówiąc, pozwalają pisać kod, który działa z typami podstawowymi i nie jest powiązany z faktyczną implementacją. Pozwala to na modyfikację implementacji później bez konieczności zmiany zbyt dużej ilości kodu.
Oto inny przykład. Powiedzmy, że mam klasę, która implementuje program rejestrujący, CLog. Ta klasa zapisuje do pliku na dysku lokalnym. Zaczynam używać tej klasy w moim dotychczasowym 100 000 wierszy kodu. Wszędzie wokoło. Życie jest dobre, dopóki ktoś nie powie: hej, napiszmy do bazy danych zamiast pliku. Teraz tworzę nową klasę, nazwijmy ją CDbLog i zapisuję w bazie danych. Czy potrafisz sobie wyobrazić trudność przejścia przez 100 000 linii i zmiany wszystkiego z CLog na CDbLog? Alternatywnie, mógłbym mieć:
Jeśli cały kod używa interfejsu ILogger, wszystko, co musiałbym zmienić, to wewnętrzna implementacja CLogFactory :: GetLog (). Reszta kodu działałaby po prostu automatycznie, bez konieczności kiwnięcia palcem.
Aby uzyskać więcej informacji na temat interfejsów i dobrego projektowania OO, zdecydowanie polecam zwinne zasady, wzorce i praktyki wuja Boba w języku C # . Książka jest pełna przykładów, które wykorzystują abstrakcje i zawierają wyjaśnienia wszystkiego w prostym języku.
źródło
Nigdy. Co gorsza, są absolutną suką, której można się pozbyć, więc popełnienie tego błędu może cię prześladować przez wiele, wiele lat.
Różnica między klasami abstrakcyjnymi a interfejsami jest absolutnie niczym w C ++. Zwykle masz interfejsy do określania niektórych zachowań klasy pochodnej, ale bez konieczności określania wszystkich z nich. Dzięki temu kod jest bardziej elastyczny, ponieważ możesz zamienić dowolną klasę, która spełnia bardziej ograniczoną specyfikację. Interfejsy w czasie wykonywania są używane, gdy potrzebujesz abstrakcji w czasie wykonywania.
źródło
Singleton jest przydatny, gdy nie chcesz wielu kopii konkretnego obiektu, musi istnieć tylko jedna instancja tej klasy - jest używana w przypadku obiektów, które utrzymują stan globalny, muszą w jakiś sposób radzić sobie z kodem nieprzynoszącym ponownie itp.
Singleton, który ma stałą liczbę 2 lub więcej instancji, jest multitonem , pomyśl o puli połączeń z bazą danych itp.
Interfejs określa dobrze zdefiniowany interfejs API, który pomaga modelować interakcję między obiektami. W niektórych przypadkach możesz mieć grupę klas, które mają pewne wspólne funkcje - jeśli tak, zamiast duplikować je w implementacjach, możesz dodać definicje metod do interfejsu, zamieniając je w klasę abstrakcyjną .
Możesz nawet mieć klasę abstrakcyjną, w której wszystkie metody są zaimplementowane, ale oznaczysz ją jako abstrakcyjną, aby wskazać, że nie powinna być używana w obecnej postaci bez podklasy.
Uwaga: Interfejs i klasa abstrakcyjna nie są bardzo różne w świecie C ++ z wielokrotnym dziedziczeniem itp., Ale mają różne znaczenia w Javie i in.
źródło
Jeśli przestaniesz o tym myśleć, chodzi o polimorfizm. Chcesz raz napisać fragment kodu, który może zrobić więcej niż myślisz, w zależności od tego, co go przekażesz.
Załóżmy, że mamy funkcję podobną do następującego kodu w języku Python:
Zaletą tej funkcji jest to, że będzie ona w stanie obsłużyć dowolną listę obiektów, o ile obiekty te implementują metodę „printToScreen”. Możesz przekazać mu listę szczęśliwych widżetów, listę smutnych widżetów, a nawet listę zawierającą ich mieszankę, a funkcja foo nadal będzie mogła poprawnie wykonywać swoje zadania.
Odnosimy się do tego typu ograniczenia konieczności posiadania zestawu metod zaimplementowanych (w tym przypadku printToScreen) jako interfejsu, a obiekty, które implementują wszystkie metody, mają zaimplementować interfejs.
Gdybyśmy mówili o dynamicznym języku typu „kaczka”, takim jak Python, w zasadzie już byśmy skończyli. Jednak system typów statycznych C ++ wymaga, abyśmy nadali klasę obiektom w naszej funkcji i będzie on mógł pracować tylko z podklasami tej klasy początkowej.
W naszym przypadku jedynym powodem, dla którego istnieje klasa Printable, jest miejsce dla metody printToScreen. Ponieważ nie istnieje wspólna implementacja między klasami, które implementują metodę printToScreen, sensowne jest uczynienie z Printable klasy abstrakcyjnej, która jest używana tylko jako sposób grupowania podobnych klas we wspólnej hierarchii.
W C ++ koncepcje klas absctract i interfejsów są nieco rozmyte. Jeśli chcesz je lepiej zdefiniować, myślisz o klasach abstrakcyjnych, podczas gdy interfejsy zwykle oznaczają bardziej ogólną, międzyjęzykową koncepcję zestawu widocznych metod, które przedstawia obiekt. (Chociaż niektóre języki, takie jak Java, używają terminu interfejs, aby odnosić się do czegoś bardziej bezpośrednio, jak abstrakcyjna klasa podstawowa)
Zasadniczo konkretne klasy określają sposób implementacji obiektów, podczas gdy klasy abstrakcyjne określają sposób, w jaki łączą się one z resztą kodu. Aby twoje funkcje były bardziej polimorficzne, powinieneś próbować otrzymać wskaźnik do abstrakcyjnej nadklasy, ilekroć byłoby to uzasadnione.
Jeśli chodzi o Singletony, są one naprawdę bezużyteczne, ponieważ często można je zastąpić tylko grupą statycznych metod lub zwykłych starych funkcji. Czasami jednak masz jakieś ograniczenia, które zmuszają cię do korzystania z obiektu, nawet jeśli tak naprawdę nie chcesz go używać, więc tupot singletonu jest odpowiedni.
BTW, niektóre osoby mogły komentować, że słowo „interfejs” ma szczególne znaczenie w języku Java. Myślę jednak, że na razie lepiej trzymać się bardziej ogólnej definicji.
źródło
Interfejsy
Trudno zrozumieć cel narzędzia, które rozwiązuje problem, którego nigdy nie miałeś. Nie rozumiałem interfejsów przez jakiś czas po rozpoczęciu programowania. Zrozumiemy, co zrobili, ale nie wiedziałem, dlaczego chcesz z nich skorzystać.
Oto problem - wiesz, co chcesz zrobić, ale masz na to wiele sposobów lub możesz zmienić sposób, w jaki to zrobisz później. Byłoby miło, gdybyś mógł wcielić się w rolę nieświadomego menedżera - wydawać zamówienia i osiągać pożądane wyniki bez dbania o to, jak to się robi.
Załóżmy, że masz małą witrynę internetową i zapisujesz wszystkie informacje o swoich użytkownikach w pliku csv. Nie jest to najbardziej wyrafinowane rozwiązanie, ale działa wystarczająco dobrze, aby przechowywać dane użytkownika twojej mamy. Później Twoja strona startuje i masz 10 000 użytkowników. Może nadszedł czas, aby użyć odpowiedniej bazy danych.
Gdybyś na początku był sprytny, zobaczyłbyś, że to nadchodzi, i nie wykonałeś połączeń, aby zapisać bezpośrednio w csv. Zamiast tego pomyślałbyś o tym, co musisz zrobić, bez względu na to, jak to zostało zaimplementowane. Powiedzmy
store()
iretrieve()
. Państwo dokonaćPersister
interfejs z abstrakcyjnych metodstore()
iretrieve()
i utworzyćCsvPersister
podklasę że faktycznie implementuje te metody.Później możesz utworzyć plik,
DbPersister
który implementuje faktyczne przechowywanie i pobieranie danych zupełnie inaczej niż w przypadku klasy csv.Wspaniałą rzeczą jest to, że wszystko, co musisz teraz zrobić, to zmienić
do
i gotowe. Twoje wezwania do
prst.store()
iprst.retrieve()
nadal będą działać, są po prostu inaczej przetwarzane „za kulisami”.Teraz nadal musiałeś tworzyć implementacje cvs i db, więc nie doświadczyłeś jeszcze luksusu bycia szefem. Rzeczywiste korzyści są widoczne, gdy używasz interfejsów utworzonych przez kogoś innego. Jeśli ktoś inny był na tyle uprzejmy, aby stworzyć
CsvPersister()
iDbPersister()
już, to wystarczy wybrać jeden i wywołać niezbędne metody. Jeśli zdecydujesz się użyć drugiego później lub w innym projekcie, już wiesz, jak to działa.Jestem naprawdę zardzewiały na moim C ++, więc użyję tylko ogólnych przykładów programowania. Kontenery są doskonałym przykładem tego, jak interfejsy ułatwiają życie.
Możesz mieć
Array
,LinkedList
,BinaryTree
, z itp wszystkie podklasyContainer
, która ma metody, takie jakinsert()
,find()
,delete()
.Teraz, gdy dodajesz coś na środku połączonej listy, nie musisz nawet wiedzieć, czym jest połączona lista. Wystarczy zadzwonić,
myLinkedList->insert(4)
a magicznie iteruje się po liście i umieszcza ją tam. Nawet jeśli wiesz, jak działa połączona lista (co naprawdę powinieneś), nie musisz sprawdzać jej konkretnych funkcji, ponieważ prawdopodobnie już wiesz, co to za korzystanie z innejContainer
wcześniej.Klasy abstrakcyjne
Klasy abstrakcyjne są bardzo podobne do interfejsów (technicznie interfejsy są klasami abstrakcyjnymi, ale tutaj mam na myśli klasy podstawowe, które mają rozwinięte niektóre z ich metod.
Załóżmy, że tworzysz grę i musisz wykryć, kiedy wrogowie znajdują się w odległości uderzenia gracza. Możesz utworzyć klasę podstawową,
Enemy
która ma metodęinRange()
. Chociaż istnieje wiele różnych cech przeciwników, metoda sprawdzania ich zasięgu jest spójna. Dlatego twojaEnemy
klasa będzie miała dopracowaną metodę sprawdzania zasięgu, ale czyste metody wirtualne dla innych rzeczy, które nie mają podobieństw między typami wroga.Zaletą jest to, że jeśli zepsujesz kod wykrywania zasięgu lub chcesz go ulepszyć, musisz go zmienić tylko w jednym miejscu.
Oczywiście istnieje wiele innych powodów interfejsów i abstrakcyjnych klas bazowych, ale są to niektóre powody, dla których możesz z nich korzystać.
Singletony
Używam ich od czasu do czasu i nigdy mnie nie spalili. Nie oznacza to, że w pewnym momencie nie zrujnują mi życia na podstawie doświadczeń innych ludzi.
Oto dobra dyskusja na temat stanu globalnego od bardziej doświadczonych i ostrożnych ludzi: Dlaczego państwo globalne jest tak złe?
źródło
W królestwie zwierząt istnieją różne zwierzęta, które są ssakami. Tutaj ssak jest klasą podstawową i wywodzą się z niego różne zwierzęta.
Czy widziałeś kiedyś przechodzącego ssaka? Tak, wiele razy, jestem pewien - jednak były one wszystkie rodzaje ssaków nie byli?
Nigdy nie widziałeś czegoś, co dosłownie było tylko ssakiem. Były to wszystkie typy ssaków.
Klasa ssak jest wymagana do zdefiniowania różnych cech i grup, ale nie istnieje jako istota fizyczna.
Dlatego jest to abstrakcyjna klasa bazowa.
Jak poruszają się ssaki? Czy chodzą, pływają, latają itp.?
Nie ma sposobu, aby wiedzieć na poziomie ssaków, ale wszystkie ssaki muszą się w jakiś sposób poruszać (powiedzmy, że jest to prawo biologiczne, aby ułatwić przykład).
Dlatego MoveAround () jest funkcją wirtualną, ponieważ każdy ssak, który wywodzi się z tej klasy, musi być w stanie zaimplementować ją inaczej.
Jednak fakt, że każdy ssak MUSI zdefiniować MoveAround, ponieważ wszystkie ssaki muszą się poruszyć i nie można tego zrobić na poziomie ssaka. Musi być zaimplementowany przez wszystkie klasy potomne, ale tam nie ma znaczenia w klasie podstawowej.
Dlatego MoveAround jest czysto wirtualną funkcją.
Jeśli masz całą klasę, która zezwala na aktywność, ale nie jest w stanie zdefiniować na najwyższym poziomie, jak to zrobić, wszystkie funkcje są czysto wirtualne i jest to interfejs.
Na przykład - jeśli mamy grę, w której zakodujesz robota i prześlesz go mi do walki na polu bitwy, muszę znać nazwy funkcji i prototypy, które należy wywołać. Nie dbam o to, jak wdrażasz go po swojej stronie, dopóki „interfejs” jest wyraźny. Dlatego mogę zapewnić ci interfejs, z którego będziesz mógł napisać swojego zabójczego robota.
źródło