Program CS mojej szkoły unika jakiejkolwiek wzmianki o programowaniu obiektowym, więc czytam sam, aby go uzupełnić - w szczególności Object Oriented Software Construction autorstwa Bertrand Meyer.
Meyer wielokrotnie podkreśla, że klasy powinny ukrywać jak najwięcej informacji o ich implementacji, co ma sens. W szczególności wielokrotnie argumentuje, że atrybuty (tj. Statyczne, niepoliczone właściwości klas) i procedury (właściwości klas, które odpowiadają wywołaniom funkcji / procedur) powinny być od siebie nierozróżnialne.
Na przykład, jeśli klasa Person
ma atrybut age
, twierdzi, że z notacji nie powinno być możliwe stwierdzenie, czy Person.age
odpowiada on wewnętrznie czemuś podobnemu, return current_year - self.birth_date
czy po prostu return self.age
, gdzie self.age
został zdefiniowany jako atrybut stały. To ma dla mnie sens. Jednak dalej twierdzi, że:
Zostanie opracowana standardowa dokumentacja klienta dla klasy, zwana krótką formą klasy, aby nie ujawniać, czy dana funkcja jest atrybutem, czy funkcją (w przypadkach, w których może to być albo).
tzn. twierdzi, że nawet dokumentacja dla klasy powinna unikać określania, czy „moduł pobierający” wykonuje jakiekolwiek obliczenia.
Nie podążam za tym. Czy dokumentacja nie jest jedynym miejscem, w którym ważne byłoby poinformowanie użytkowników o tym rozróżnieniu? Gdybym miał zaprojektować bazę danych wypełnioną Person
obiektami, czy nie byłoby ważne, aby wiedzieć, czy Person.age
jest to kosztowne wywołanie, więc mogłem zdecydować, czy zaimplementować dla niego jakąś pamięć podręczną? Czy źle zrozumiałem to, co mówi, czy jest to szczególnie ekstremalny przykład filozofii projektowania OOP?
źródło
Odpowiedzi:
Nie sądzę, aby punkt Meyera polegał na tym, że nie powinieneś informować użytkownika o kosztownej operacji. Jeśli twoja funkcja ma trafić do bazy danych lub poprosić serwer WWW i poświęcić kilka godzin na przetwarzanie, inny kod będzie musiał o tym wiedzieć.
Ale programista używający twojej klasy nie musi wiedzieć, czy zaimplementowałeś:
lub:
Charakterystyka wydajności między tymi dwoma podejściami jest tak minimalna, że nie powinna mieć znaczenia. Koder używający twojej klasy naprawdę nie powinien dbać o to, co masz. Taki jest punkt Meyera.
Ale nie zawsze tak jest, na przykład załóżmy, że masz metodę rozmiaru na kontenerze. Można to wdrożyć:
lub
lub może to być:
Różnica między dwoma pierwszymi naprawdę nie powinna mieć znaczenia. Ale ostatni może mieć poważne konsekwencje w zakresie wydajności. Właśnie dlatego STL mówi, że tak
.size()
jestO(1)
. Nie dokumentuje dokładnie, w jaki sposób obliczany jest rozmiar, ale daje mi cechy wydajności.Więc : problemy z wydajnością dokumentów. Nie dokumentuj szczegółów implementacji. Nie obchodzi mnie, w jaki sposób std :: sort sortuje moje rzeczy, o ile robi to odpowiednio i wydajnie. Twoja klasa nie powinna także dokumentować, jak to oblicza, ale jeśli coś ma nieoczekiwany profil wydajności, udokumentuj to.
źródło
// O(n) Traverses the entire user list.
len
nie robi tego ... (W niektórych sytuacjach jestO(n)
tak, jak dowiedzieliśmy się w projekcie na studiach, kiedy zasugerowałem przechowywanie długości zamiast ponownego obliczania jej przy każdej iteracji pętli)O(n)
?Z punktu widzenia purystów akademickich lub CS, jest to oczywiście brak opisania w dokumentacji czegokolwiek na temat wewnętrznych aspektów implementacji funkcji. Jest tak, ponieważ użytkownik klasy idealnie nie powinien przyjmować żadnych założeń dotyczących wewnętrznej implementacji klasy. Jeśli implementacja ulegnie zmianie, idealnie żaden użytkownik tego nie zauważy - funkcja tworzy abstrakcję, a elementy wewnętrzne powinny być całkowicie ukryte.
Jednak większość programów w świecie rzeczywistym cierpi na „Prawo nieszczelnych abstrakcji” Joela Spolsky'ego , które mówi
Oznacza to, że praktycznie niemożliwe jest stworzenie pełnej czarnej skrzynki abstrakcyjnej złożonej funkcji. Typowym objawem tego są problemy z wydajnością. Dlatego w przypadku programów z prawdziwego świata może okazać się bardzo ważne, które połączenia są drogie, a które nie, a dobra dokumentacja powinna zawierać te informacje (lub powinna wskazywać, gdzie użytkownik klasy może przyjmować założenia dotyczące wydajności, a gdzie nie ).
Tak więc moja rada: dołącz informacje o potencjalnych kosztownych rozmowach, jeśli piszesz dokumenty do programu w świecie rzeczywistym, i wykluczaj je dla programu, który piszesz wyłącznie w celach edukacyjnych swojego kursu CS, biorąc pod uwagę, że należy zachować wszelkie względy dotyczące wydajności celowo poza zakresem.
źródło
Możesz pisać, jeśli dane połączenie jest drogie, czy nie. Lepiej skorzystaj z konwencji nazewnictwa, takiej jak
getAge
szybki dostęp i /loadAge
lubfetchAge
kosztowne wyszukiwanie. Zdecydowanie chcesz poinformować użytkownika, czy metoda wykonuje jakieś IO.Każdy szczegół, który podajesz w dokumentacji, jest jak umowa, którą klasa musi uszanować. Powinien informować o ważnych zachowaniach. Często zobaczysz wskazanie złożoności z dużą notacją O. Ale zwykle chcesz być krótki i do rzeczy.
źródło
Tak.
Dlatego czasami używam
Find()
funkcji, aby wskazać, że wywołanie tego może chwilę potrwać. To bardziej konwencja niż cokolwiek innego. Czas potrzebny na zwrócenie funkcji lub atrybutu nie ma znaczenia dla programu (chociaż może to być dla użytkownika), chociaż wśród programistów oczekuje się, że jeśli zostanie zadeklarowany jako atrybut, koszt wywołania powinien wynosić Niska.W każdym razie w samym kodzie powinno być wystarczająco dużo informacji, aby wywnioskować, czy coś jest funkcją lub atrybutem, więc tak naprawdę nie widzę potrzeby mówienia tego w dokumentacji.
źródło
Get
metod nad atrybutami w celu wskazania operacji o większej wadze. Widziałem wystarczająco dużo kodu, w którym programiści zakładają, że właściwość jest tylko akcesorium i używają jej wiele razy zamiast zapisywania wartości w zmiennej lokalnej, a tym samym wykonują bardzo złożony algorytm więcej niż raz. Jeśli nie ma konwencji, aby nie implementować takich właściwości, a dokumentacja nie wskazuje na złożoność, to życzę powodzenia każdemu, kto musi utrzymywać taką aplikację.get
metoda jest równoważna z dostępem do atrybutu i dlatego nie jest droga.Ważne jest, aby pamiętać, że pierwsze wydanie tej książki zostało napisane w 1988 roku, na początku OOP. Ci ludzie pracowali z bardziej czysto obiektowymi językami, które są dziś powszechnie używane. Nasze najpopularniejsze obecnie języki OO - C ++, C # i Java - mają pewne dość znaczące różnice w sposobie działania wczesnych języków, bardziej czysto OO.
W języku takim jak C ++ i Java należy rozróżnić dostęp do atrybutu i wywołanie metody. Istnieje świat różnic między
instance.getter_method
iinstance.getter_method()
. Jeden faktycznie dostaje twoją wartość, a drugi nie.Kiedy pracujesz z bardziej czysto językiem OO, perswazji Smalltalk lub Ruby (która wydaje się, że jest to język Eiffla użyty w tej książce), staje się ona całkowicie prawidłową radą. Te języki będą domyślnie wywoływać dla ciebie metody. Nie ma różnicy między
instance.attribute
iinstance.getter_method
.Nie przejmowałbym się tym punktem ani nie przyjmowałbym go zbyt dogmatycznie. Cel jest dobry - nie chcesz, aby użytkownicy twojej klasy martwili się o nieistotne szczegóły implementacji - ale nie przekłada się to na składnię wielu współczesnych języków.
źródło
Jako użytkownik nie musisz wiedzieć, jak coś jest implementowane.
Jeśli wydajność stanowi problem, należy coś zrobić wewnątrz implementacji klasy, a nie wokół niej. Dlatego poprawnym działaniem jest naprawienie implementacji klasy lub zgłoszenie błędu do opiekuna.
źródło
string.length
będą one przeliczane za każdym razem, gdy się zmieni.Każda dokumentacja zorientowana na programistę, która nie informuje programistów o złożoności kosztów procedur / metod, jest wadliwa.
Szukamy metod wolnych od skutków ubocznych.
Jeśli wykonanie sposobu prowadzi złożoność czasową i / lub złożoności pamięci inne niż
O(1)
, na kartach pamięci lub czasowo ograniczone środowiskach można uznać mieć skutki uboczne .Zasada najmniejszego zaskoczenia jest naruszona, jeśli metoda robi coś zupełnie nieoczekiwanego - w tym przypadku, wyginanie pamięci lub marnowania czasu procesora.
źródło
Myślę, że dobrze go zrozumiałeś, ale myślę, że masz rację. jeśli
Person.age
jest implementowany z kosztownymi obliczeniami, to myślę, że chciałbym zobaczyć to również w dokumentacji. Może mieć znaczenie między wielokrotnym wywoływaniem go (jeśli jest to niedroga operacja) lub wywoływaniem go raz a buforowaniem wartości (jeśli jest kosztowne). Nie wiem na pewno, ale myślę, że w tym przypadku Meyer może zgodzić się na dołączenie ostrzeżenia do dokumentacji.Innym sposobem na poradzenie sobie z tym może być wprowadzenie nowego atrybutu, którego nazwa sugeruje, że może nastąpić długie obliczenie (np.
Person.ageCalculatedFromDB
), A następniePerson.age
zwrócić wartość zapisaną w pamięci podręcznej w klasie, ale nie zawsze może to być właściwe i wydaje się, że jest nadmiernie skomplikowane rzeczy, moim zdaniem.źródło
age
aPerson
, powinieneś wywołać metodę, aby uzyskać ją niezależnie. Jeśli dzwoniący zaczną robić zbyt sprytne rzeczy o połowę, aby uniknąć konieczności wykonywania obliczeń, ryzykują, że ich implementacje nie będą działać poprawnie, ponieważ przekroczyli granicę urodzin. Drogie implementacje w klasie przejawiają się jako problemy z wydajnością, które można wykorzenić przez profilowanie, a ulepszenia, takie jak buforowanie, można wykonać w klasie, w której wszyscy dzwoniący zobaczą korzyści (i poprawne wyniki).Person
klasie, ale myślę, że pytanie było zamierzone jako bardziej ogólne i toPerson.age
był tylko przykład. Prawdopodobnie w niektórych przypadkach wybór dzwoniącego miałby większy sens - być może odbiorca ma dwa różne algorytmy do obliczania tej samej wartości: jeden szybki, ale niedokładny, jeden znacznie wolniejszy, ale dokładniejszy (renderowanie 3D przychodzi na myśl jako jedno miejsce gdzie może się to zdarzyć), a dokumentacja powinna o tym wspomnieć.Dokumentacja dla klas obiektowych często wiąże się z kompromisem między zapewnieniem opiekunom klasy elastyczności w zakresie zmiany projektu, a umożliwieniem konsumentom klasy pełnego wykorzystania jej potencjału. Jeśli niezmienne klasa ma szereg właściwości, które mają pewną dokładnie związek ze sobą (na przykład
Left
,Right
iWidth
właściwości prostokąta z wyrównanymi do siatki prostokątami), można zaprojektować klasę do przechowywania dowolnej kombinacji dwóch właściwości i obliczyć trzecią, lub można zaprojektować ją do przechowywania wszystkich trzech. Jeśli nic w interfejsie nie wyjaśnia, które właściwości są przechowywane, programista klasy może być w stanie zmienić projekt w przypadku, gdy byłoby to z jakiegoś powodu pomocne. Dla kontrastu, jeśli np. Dwie właściwości są ujawnione jakofinal
pola, a trzecia nie, wówczas przyszłe wersje klasy zawsze będą musiały używać tych samych dwóch właściwości jako „podstawy”.Jeśli właściwości nie mają ścisłego związku (np. Ponieważ są
float
lubdouble
raczej nieint
), może być konieczne udokumentowanie, które właściwości „definiują” wartość klasy. Na przykład, mimo żeLeft
plusWidth
ma być równyRight
, matematyka zmiennoprzecinkowa jest często niedokładna. Na przykład załóżmy, żeRectangle
który używa typuFloat
przyjmujeLeft
iWidth
jako parametry konstruktora są konstruowane zLeft
podanymi jako1234567f
iWidth
jako1.1f
. Najlepszafloat
reprezentacja sumy to 1234568.125 [która może być wyświetlana jako 1234568.13]; następny mniejszyfloat
to 1234568.0. Jeśli klasa faktycznie przechowujeLeft
iWidth
, może zgłosić wartość szerokości, tak jak została podana. Jeśli jednak konstruktor obliczaneRight
w oparciu o przekazany wLeft
iWidth
, a później obliczaneWidth
w oparciuLeft
iRight
, byłoby zgłosić szerokość jak1.25f
zamiast jak przekazany w1.1f
.W przypadku klas, które można modyfikować, rzeczy mogą być jeszcze bardziej interesujące, ponieważ zmiana jednej z powiązanych ze sobą wartości spowoduje zmianę co najmniej jednej innej, ale nie zawsze będzie jasne, która z nich. W niektórych przypadkach może to być najlepiej, aby uniknąć konieczności metod, które „zestaw” pojedyncza nieruchomość jako taka, lecz obaj mają metody do np
SetLeftAndWidth
alboSetLeftAndRight
, albo wyjaśnić, jakie właściwości są określone i które zmieniają się (npMoveRightEdgeToSetWidth
,ChangeWidthToSetLeftEdge
lubMoveShapeToSetRightEdge
) .Czasami przydatne może być posiadanie klasy, która śledzi, które wartości właściwości zostały określone, a które obliczone na podstawie innych. Na przykład klasa „moment w czasie” może obejmować czas bezwzględny, czas lokalny i przesunięcie strefy czasowej. Podobnie jak w przypadku wielu takich typów, biorąc pod uwagę dowolne dwie informacje, można obliczyć trzeci. Wiedząc któryczęść informacji została obliczona, jednak czasami może być ważna. Załóżmy na przykład, że zdarzenie zostało zarejestrowane jako „o 17:00 UTC, strefa czasowa -5, godzina lokalna 12:00 pm”, a jeden później odkrywa, że strefa czasowa powinna mieć wartość -6. Jeśli wiadomo, że UTC został zarejestrowany poza serwerem, rekord należy poprawić do „18:00 UTC, strefa czasowa -6, czas lokalny 12:00”; jeśli ktoś wprowadził czas lokalny poza zegarem, powinien to być „17:00 UTC, strefa czasowa -6, czas lokalny 11:00”. Nie wiedząc jednak, czy czas globalny czy lokalny należy uznać za „bardziej wiarygodny”, nie można jednak ustalić, którą korektę należy zastosować. Gdyby jednak zapis śledził, który czas został określony, zmiany strefy czasowej mogłyby pozostawić tę jedną w spokoju, zmieniając drugą.
źródło
Wszystkie te zasady ukrywania informacji w klasach mają sens, zakładając, że trzeba chronić się przed kimś spośród użytkowników klasy, który popełni błąd, tworząc zależność od wewnętrznej implementacji.
Dobrze jest wbudować taką ochronę, jeśli klasa ma taką widownię. Ale kiedy użytkownik pisze wywołanie funkcji w twojej klasie, ufa ci swoim kontem bankowym w czasie wykonywania.
Oto coś, co często widzę:
Obiekty mają „zmodyfikowany” bit, mówiąc, że są w pewnym sensie nieaktualne. Jest to dość proste, ale mają obiekty podrzędne, więc łatwo jest „zmodyfikować” funkcję, która sumuje wszystkie obiekty podrzędne. Następnie, jeśli istnieje wiele warstw podległych obiektów (czasami współużytkujących ten sam obiekt więcej niż jeden raz), proste „pobranie” właściwości „zmodyfikowanej” może w końcu zająć znaczną część czasu wykonania.
Kiedy obiekt jest w jakiś sposób modyfikowany, zakłada się, że inne obiekty rozrzucone po oprogramowaniu wymagają „powiadomienia”. Może się to odbywać na wielu warstwach struktury danych, okien itp. Napisanych przez różnych programistów, a czasem powtarzających się w nieskończonych rekurencjach, przed którymi należy się zabezpieczyć. Nawet jeśli wszyscy autorzy tych procedur obsługi powiadomień są dość ostrożni, aby nie marnować czasu, cała złożona interakcja może skończyć się nieprzewidzianym i boleśnie dużym ułamkiem czasu wykonania, a założenie, że jest to po prostu „konieczne”, jest beztroskie.
SO, lubię widzieć klasy, które prezentują ładny, abstrakcyjny interfejs dla świata zewnętrznego, ale lubię mieć pojęcie o tym, jak działają, choćby po to, aby zrozumieć, jaką pracę oszczędzają. Ale poza tym wydaje mi się, że „mniej znaczy więcej”. Ludzie są tak zachwyceni strukturą danych, że myślą, że im więcej, tym lepiej, a kiedy robię dostrajanie wydajności, powszechnym ogromnym powodem problemów z wydajnością jest niewolnicze przestrzeganie rozdętych struktur danych zbudowanych w sposób, w jaki ludzie są uczeni.
Więc idź.
źródło
Dodanie szczegółów implementacji, takich jak „oblicz lub nie” lub „informacje o wydajności”, utrudnia synchronizację kodu i dokumentu .
Przykład:
Jeśli masz metodę „kosztowną”, czy chcesz udokumentować „kosztowną” także dla wszystkich klas, które korzystają z tej metody? co jeśli zmienisz implementację, aby nie była już droga. Czy chcesz zaktualizować te informacje również dla wszystkich konsumentów?
Oczywiście miło jest, aby opiekun kodu pobierał wszystkie ważne informacje z dokumentacji kodu, ale nie podoba mi się dokumentacja, która twierdzi, że coś jest już nieważne (niesynchronizowane z kodem)
źródło
Po zaakceptowaniu odpowiedzi dochodzi do wniosku:
a samodokumentujący się kod jest uważany za lepszy niż dokumentacja , stąd nazwa metody powinna określać wszelkie nietypowe wyniki wydajności.
Więc nadal
Person.age
nareturn current_year - self.birth_date
ale jeśli metoda wykorzystuje pętlę obliczyć wiek (tak):Person.calculateAge()
źródło