Drugie źródło nieporozumień – funkcje ogólne

Inną dużą różnicą w porównaniu z popularnymi językami obiektowymi, takimi jak te wspomniane wcześniej, jest to, że R implementuje polimorfizm parametryczny, znany również jako funkcje ogólne, co oznacza, że ​​metody należą do funkcji, a nie do klas. Funkcje ogólne pozwalają na użycie tej samej nazwy dla wielu różnych funkcji, z wieloma różnymi zestawami argumentów, z wielu różnych klas. Oznacza to, że składnia wywołania metody klasy różni się od składni normalnie połączonej w łańcuchy, którą można znaleźć w innych językach (zwykle zaimplementowana z „.” (Kropką) między klasą a metodą, którą chcemy wywołać), co nazywa się przekazywaniem komunikatów. Wywołania metod w R wyglądają jak wywołania funkcji, a R musi wiedzieć, które nazwy wymagają prostych wywołań funkcji, a które nazwy wymagają wywołań metod. Jeśli przeczytałeś poprzednie sekcje, powinieneś zrozumieć, dlaczego jest to ważne. R musi mieć mechanizm umożliwiający rozróżnienie tego, co ma robić. Ten mechanizm nazywa się funkcjami ogólnymi. Korzystając z funkcji ogólnych, rejestrujemy pewne nazwy, które mają być traktowane jako metody w języku R i działają one jako dyspozytorzy. Kiedy wywołujemy zarejestrowane funkcje ogólne, R zajrzy do łańcucha atrybutów obiektu, który jest przekazywany w wywołaniu, i będzie szukał funkcji, które pasują do wywołania metody dla typu tego obiektu; jeśli znajdzie taki, nazwie go. Być może zauważyłeś, że funkcje plot () i summary () mogą zwracać różne wyniki, w zależności od obiektów, które są do nich przekazywane (na przykład ramka danych lub instancja modelu liniowego). Dzieje się tak, ponieważ są to funkcje ogólne, które implementują polimorfizm. Ten sposób pracy zapewnia użytkownikom proste interfejsy, które mogą znacznie uprościć ich zadania. Na przykład, jeśli eksplorujesz nowy pakiet iw pewnym momencie otrzymujesz wynik pochodzący z pakietu, spróbuj wywołać plot (result)) i możesz być zaskoczony, gdy otrzymasz jakiś rodzaj wykresu, który ma sens. To nie jest powszechne w innych językach. Podczas programowania zorientowanego obiektowo za pomocą modeli S3 i S4 języka R należy pamiętać, że nie należy wywoływać metod bezpośrednio, ale zamiast tego deklarować odpowiadające im funkcje ogólne i wywoływać je. Na początku może to być trochę zagmatwane, ale to tylko jedna z unikalnych cech R, do której przyzwyczajasz się z czasem

Pierwsze źródło nieporozumień – różne modele obiektów

Sposób pracy z programowaniem obiektowym w języku R różni się od tego, co możesz zobaczyć w innych językach, takich jak Python, Java, C ++ i wielu innych. W większości języki te mają jeden model obiektów, z którego korzystają wszyscy. W przypadku R zwróć uwagę, że pisaliśmy modele obiektów w liczbie mnogiej. Dzieje się tak, ponieważ R jest bardzo specjalnym językiem i ma różne sposoby implementacji systemów obiektowych. W szczególności R ma następujące modele obiektów – S3, S4, klasy referencyjne, R6 i typy podstawowe. W następnych sekcjach zagłębimy się w modele S3, S4 i R6. Teraz pokrótce zajmiemy się klasami referencyjnymi i typami podstawowymi. Klasy referencyjne (RC) to model obiektowy w języku R, który nie wymaga zewnętrznych bibliotek i jest najbardziej podobny do dobrze znanego modelu obiektowego, który można znaleźć w Pythonie, Javie lub C ++. Implementuje przekazywanie komunikatów tak jak te języki, co oznacza, że ​​metody należą do klas, a nie do funkcji, a obiekty są modyfikowalne, co oznacza, że ​​dane instancji mogą zmieniać się w miejscu zamiast tworzyć kopie ze zmodyfikowanymi danymi. Nie będziemy zagłębiać się w ten model obiektowy, ponieważ R6 wydaje się być czystszą implementacją takiego modelu. Jednak R6 wymaga zewnętrznego pakietu, jak zobaczymy później, co nie stanowi problemu i dlatego jest preferowane. Typy podstawowe nie są same w sobie modelem obiektowym. Są to implementacje C, które działają w tle języka R i są używane do tworzenia innych modeli obiektów na nich. Tylko główny zespół programistów R może dodawać nowe klasy do tego modelu i robią to bardzo rzadko (zanim to zrobią, może minąć wiele lat). Ich użycie jest bardzo zaawansowane i nie będziemy się też w nie zagłębiać. Decyzja o tym, jakiego modelu obiektowego użyć, jest ważna i omówimy ją więcej, gdy pokażemy, jak z nimi pracować. Ogólnie rzecz biorąc, sprowadza się to do kompromisu między elastycznością, formalnością i czystością kodu

Przedstawiamy trzy modele obiektów w R – S3, S4 i S6

Teraz, gdy masz już podstawową wiedzę na temat ogólnych pojęć zorientowanych obiektowo, zajmiemy się własnymi modelami obiektów R. Istnieją dwa główne źródła nieporozumień podczas programowania obiektowego w języku R. Zanim zaczniemy tworzyć kod, wyjaśnimy, jakie są te źródła zamieszania. Następnie opracujemy mały przykład ilustrujący dziedziczenie, skład, polimorfizm i hermetyzację w modelach obiektowych R S3, S4 i R6. Ten sam przykład zostanie użyty dla wszystkich trzech modeli, aby czytelnik mógł precyzyjnie wskazać różnice. W szczególności modelujemy a Square dziedziczący po Rectangle, które z kolei składa się z Color.

Interfejsy, factor i ogólnie wzorce

Interfejs to część klasy, która jest udostępniana do użytku przez inne obiekty. W szczególności jest to zestaw definicji publicznych metod klasy. Oczywiście, im więcej metod publicznych obiekt ma, tym więcej obowiązków i mniej elastyczności ma wobec świata zewnętrznego. Zauważ, że interfejs nie zawiera żadnych szczegółów dotyczących implementacji; jest to tylko kontrakt, który definiuje, jakie dane wejściowe i jakie dane wyjściowe są oczekiwane po wywołaniu metody. Czasami chcesz dać sobie swobodę zmiany obiektu dla danego zadania zgodnie z kontekstem. Wiesz, że tak długo, jak interfejsy obiektów, które chcesz wymieniać są takie same, wszystko powinno być w porządku (oczywiście przy założeniu, że programiści implementują wspomniane interfejsy poprawnie). Jeśli nie planujesz tego z wyprzedzeniem, zmiana tych obiektów może być trudnym zadaniem. Tutaj do gry wchodzą factors. Factors to sposób na wybranie w czasie wykonywania i zgodnie z kontekstem, którego obiektu użyć z zestawu predefiniowanych opcji. Factors działają w zasadzie jak instrukcje, które wybierają, której klasy użyć do zadania na podstawie pewnych warunków. Są sposobem na zainwestowanie trochę więcej wysiłku dzisiaj, aby zaoszczędzić sobie sporo wysiłku później, kiedy zdecydujesz się użyć innego obiektu dla tego samego interfejsu. Powinny być używane tam, gdzie spodziewasz się, że w przyszłości będziesz używać różnego rodzaju obiektów. Factors to jeden z wielu znanych wzorców programowania zorientowanego obiektowo. Wzorce te są opracowywane przez osoby z dużym doświadczeniem w podejmowaniu decyzji projektowych i jako takie wiedzą, jakie rozwiązania mogą być ogólnie dobre dla określonych typów problemów. Dokumentowanie tych wzorców jest bardzo przydatne i pozwala wielu osobom zaoszczędzić dużo czasu i wysiłku, ponieważ nie muszą odkrywać koła na nowo w ich własnym kontekście.

Metody publiczne i prywatne

Metody to funkcje zawarte w klasach i na ogół będą publiczne lub prywatne. Ogólnie metody mają dostęp do danych klas (które powinny być hermetyzowane z dala od innych obiektów), a także do swoich metod publicznych i prywatnych. Metody publiczne są widoczne dla innych obiektów i powinny być jak najbardziej stabilne, ponieważ inne obiekty mogą zacząć od nich zależeć. Jeśli je zmienisz, możesz nieoczekiwanie przerwać działanie innego obiektu. Metody prywatne są widoczne tylko dla samej instancji, co oznacza, że ​​inne obiekty nie mogą (lub nie powinny, jak ma to miejsce w przypadku języka R) bezpośrednio wywoływać te metody. Prywatne metody mogą zmieniać się tak często, jak to konieczne. Metody publiczne wykorzystują inne metody, publiczne lub prywatne, w celu dalszego delegowania zachowania. Ta delegacja rozbija problem na bardzo małe części, które są łatwo zrozumiałe, a programista zastrzega sobie prawo do modyfikowania prywatnych metod według własnego uznania. Inne obiekty nie powinny na nich polegać. Zwróć uwagę, że technicznie rzecz biorąc, tylko metody publiczne istnieją w R. W jednym z modeli obiektowych R można ukryć metody, a pod innym można je umieścić w innym środowisku, ale to nie czyni ich niedostępnymi, jak w przypadku metod prywatnych w inne języki. Wychodząc z tego, nie zajmujemy się również pojęciem metod chronionych, które są metodami widocznymi dla klasy i jej podklas. Nawet jeśli technicznie nie ma prywatnych metod w R, będziemy programować tak, jakby były. Brak jakiegoś kompilatora lub mechanizmu sprawdzającego błędy, który powiedziałby ci, że korzystasz z prywatnych metod, kiedy nie powinieneś, nie jest wymówką, aby to robić. Powinieneś zawsze tworzyć kod wysokiej jakości, nawet jeśli nie jest to wyraźnie wymuszone przez mechanizmy językowe.

To, co powiedzieliśmy wcześniej, sugeruje, że powinieneś uczynić swoje obiekty tak prywatnymi, jak to tylko możliwe, aby były spójne i oddzielone, co jest fantazyjnym określeniem samodzielności i niezależności. Innymi słowy, spróbuj maksymalnie zredukować liczbę metod w swoich obiektach. Oczywiście spójność i oddzielenie od sprzężeń są znacznie bardziej ogólnymi pomysłami niż tylko zmniejszenie liczby prywatnych metod, ale to dobry początek.

Klasy i konstruktory

Obiekty muszą być w jakiś sposób zdefiniowane, abyśmy mogli wygenerować z nich określone instancje. Najpowszechniejszym sposobem dostarczania tych definicji są klasy. Klasa to fragment kodu, który zawiera definicję obiektu, w tym zachowanie, które oferuje w odpowiedzi na komunikaty z innych obiektów, a także dane wewnętrzne potrzebne do zapewnienia tego zachowania. Zachowanie klasy jest implementowane w jej metodach. Więcej na ten temat w następnej sekcji. Klasy muszą zostać utworzone w pewnym momencie i właśnie tam wchodzą w grę konstruktorzy. W większości przypadków, gdy tworzysz instancję klasy, chcesz, aby zawierała ona pewne dane o sobie. Te dane są przypisywane do klasy, gdy są tworzone za pomocą jej konstruktora. W szczególności konstruktor to funkcja, której zadaniem jest utworzenie instancji klasy z określonym zestawem danych. Jak wiesz, te dane powinny być przechowywane wewnątrz obiektu, a inne obiekty nie powinny bezpośrednio oddziaływać z tymi danymi. Zamiast tego obiekt powinien oferować metody publiczne, których mogą używać inne obiekty, aby uzyskać potrzebne dane lub zachowanie.

Hierarchie

Hierarchie można tworzyć na dwa sposoby – dziedziczenie i skład. Ideą dziedziczenia jest tworzenie nowych klas jako wyspecjalizowanych wersji starych. Klasy specjalistyczne są podklasami, a bardziej ogólne to nadklasy. Ten typ relacji jest często nazywany relacją typu is-a, ponieważ podklasa jest typem nadklasy. Na przykład lew jest typem zwierzęcia, więc zwierzę byłoby nadklasą, a lew podklasą. Inny typ relacji jest znany jako relacja ma-a. To znaczy, że jedna klasa ma instancje innej klasy. Na przykład samochód ma koła. Nie powiedzielibyśmy, że koła są rodzajem samochodu, więc nie ma tam dziedziczenia, ale powiedzielibyśmy, że są częścią samochodu, co implikuje skład. Są przypadki, w których nie jest tak jasne, czy relacja powinna być modelowana z dziedziczeniem, czy z kompozycją, i w takich przypadkach powinieneś zdecydować się na kontynuację kompozycji. Ogólnie rzecz biorąc, ludzie zgadzają się, że kompozycja jest znacznie bardziej elastycznym sposobem projektowania systemu i że dziedziczenie powinno być używane tylko wtedy, gdy trzeba modelować specjalizację klasy. Zwróć uwagę, że kiedy projektujesz swoje systemy z kompozycją zamiast dziedziczenia, obiekty przyjmują różne role i stają się bardziej podobne do narzędzi. To dobrze, ponieważ można je łatwo połączyć ze sobą i w razie potrzeby wymienić, a także zwykle uzyskuje się większą liczbę mniejszych klas. Teraz, gdy rozumiesz podstawowe idee programowania zorientowanego obiektowo, możesz zdać sobie sprawę z mocy, jaką daje połączenie tych pomysłów. Jeśli masz system, który obejmuje zachowanie i tylko publicznie oferuje to, co jest potrzebne innym do prawidłowego działania, który może dynamicznie odpowiadać na abstrakcyjne pomysły za pomocą poprawnych i konkretnych działań oraz umożliwia interakcję hierarchii koncepcji z innymi hierarchiami pojęć, możesz być spokojny że możesz sobie poradzić ze złożonością. W kolejnych akapitach wyjaśnimy kilka bardziej przyziemnych koncepcji, które są podstawowymi elementami składowymi większości systemów zorientowanych obiektowo i które musisz zrozumieć, aby móc postępować zgodnie z kodem rozwiniemy się na przykład.

Wielopostaciowość

Polimorfizm jest prawdopodobnie najpotężniejszą cechą języków programowania zorientowanych obiektowo, obok ich obsługi abstrakcji, i to właśnie odróżnia programowanie obiektowe od bardziej tradycyjnego programowania z abstrakcyjnymi typami danych. Polimorfizm dosłownie oznacza wiele form i do tego właśnie służy w programowaniu obiektowym. Ta sama nazwa będzie oznaczać różne znaczenia, w zależności od kontekstu, w jakim jest używana, tak jak w przypadku naszych języków naturalnych. Pozwala to na znacznie czystsze i zrozumiałe abstrakcje, a także kod. Mówiąc najprościej, polimorfizm można zaimplementować na dwa różne sposoby: od wewnątrz lub od zewnątrz obiektów. Gdy jest zaimplementowany z wewnątrz obiektów, każdy obiekt musi zawierać definicję tego, jak będzie radził sobie z danym komunikatem. Jest to najpopularniejsza metoda i można ją znaleźć w Javie lub Pythonie. R jest pod tym względem bardzo wyjątkowy i realizuje podejście zewnętrzne, formalnie znane jako generyczne lub parametryczny polimorfizm. Ten sposób programowania może być frustrujący dla osób, które stosowały tylko podejście wewnętrzne, ale może być bardzo elastyczny. Podejście zewnętrzne pozwala zdefiniować ogólną metodę lub funkcję dla typów obiektów, których jeszcze nie zdefiniowałeś i których nigdy nie możesz tego zrobić. Java i Python również mogą implementować ten typ polimorfizmu, ale nie jest to ich naturą, tak jak R może implementować wnętrze, ale też nie jest to jego naturą.

Kapsułkowanie

Hermetyzacja polega na ukrywaniu elementów wewnętrznych obiektu przed innymi obiektami. Jak ujął to projektant języka C ++, Bjarne Stroustrup, enkapsulacja ukrywa informacje nie po to, aby ułatwić oszustwa, ale aby zapobiec pomyłkom. Dając innym obiektom minimalny katalog komunikatów (metod publicznych), które mogą wysłać do obiektu, pomagamy im popełniać mniej błędów i unikać wykonywania zadań, które ich nie dotyczą. To z kolei pomaga w oddzielaniu obiektów od siebie i zapewnianiu spójności wewnątrz obiektów. Powszechnym sposobem myślenia o hermetyzacji jest to, że idziesz do restauracji – wysyłasz wiadomość do kelnera z tym, co chcesz, a kelner następnie deleguje gotowanie tego, o co prosisz, szefowi restauracji. Nie musisz wchodzić do kuchni restauracji i mówić szefowi kuchni, jak ugotować posiłek, a jeśli szef kuchni chce zmienić sposób, w jaki gotuje określone danie, może to zrobić bez Twojej wiedzy. Tak samo jest z przedmiotami; nie powinni dostać się do innego obiektu i mówić mu, jak ma wykonać swoją pracę. Brzmi to dość prosto, ale w praktyce bardzo łatwo jest naruszyć tę zasadę. Porozmawiamy o tym więcej, gdy dojdziemy do sekcji Prawa Demeter w dalszej części tego rozdziału. Z technicznego punktu widzenia proces oddzielania interfejsu od implementacji nazywa się hermetyzacją.

Ważne pojęcia związane z językami obiektowymi

Istnieje wiele sposobów implementacji modelu obiektowego w językach zorientowanych obiektowo, a konkretne sposoby jego implementacji implikują różne zestawy właściwości języka. Niektóre z tych właściwości to hermetyzacja, polimorfizm, rodzaje generyczne (polimorfizm parametryczny), hierarchie (dziedziczenie i skład), podtypy i kilka innych. Są to potężne, wysokopoziomowe pomysły z precyzyjnymi definicjami, które nakładają ograniczenia na to, jak język powinien się zachowywać. Na razie nie przejmuj się nimi zbytnio; wyjaśnimy niezbędne w miarę postępów. Ciekawym ćwiczeniem jest znalezienie języków, które są uważane za zorientowane obiektowo, ale nie używają jednej lub więcej z tych właściwości. Na przykład koncepcja klasy jest niepotrzebna, co widać w przypadku języków opartych na prototypach, takich jak JavaScript. Podpisywanie jest również niepotrzebne, ponieważ nie ma sensu w językach dynamicznie typowanych, takich jak R czy Python. Moglibyśmy kontynuować i kontynuować, ale pojawia się idea, że ​​jeden język, który ma wszystkie te właściwości, nie istnieje. Ponadto jedyną właściwością, którą można znaleźć we wszystkich językach zorientowanych obiektowo, jest polimorfizm. Dlatego ludzie powszechnie mówią, że polimorfizm jest istotą programowania obiektowego. Każdy profesjonalny programista obiektowy powinien rozumieć te właściwości i mieć formalne doświadczenie z językami, które je implementują. Jednak w następnych akapitach podamy ogólne wyjaśnienie tych najbardziej powszechnych w różnych modelach obiektów R – hermetyzację, polimorfizm (z i bez typów generycznych) i hierarchie.