Jaki jest typowy sposób obsługi widoczności w bibliotekach?

12

Zastanawiałem się nad tym, kiedy używać prywatnie, a kiedy chronione w klasach. (Rozszerzę to pytanie również na końcowe klasy i metody, ponieważ jest ono powiązane. Programuję w Javie, ale myślę, że dotyczy to każdego języka OOP)

Akceptowana odpowiedź brzmi:

Dobrą zasadą jest: uczyń wszystko tak prywatnym, jak to możliwe.

I kolejny:

  1. Spraw, by wszystkie klasy były ostateczne, chyba że musisz je natychmiast podzielić na klasy.
  2. Spraw, by wszystkie metody były ostateczne, chyba że musisz od razu je podklasować i zastąpić.
  3. Spraw, aby wszystkie parametry metody były ostateczne, chyba że musisz je zmienić w ciele metody, co w większości przypadków jest dość niewygodne.

Jest to dość proste i jasne, ale co jeśli głównie piszę biblioteki (Open Source na GitHub) zamiast aplikacji?

Mógłbym wymienić wiele bibliotek i sytuacji, w których

  • Biblioteka została rozbudowana w sposób, jakiego twórcy nigdy nie pomyśleli
  • Musiało to zostać zrobione z „magią ładującą klasy” i innymi hackami z powodu ograniczeń widoczności
  • Biblioteki wykorzystano w sposób, w jaki nie zostały zbudowane, a potrzebną funkcjonalność „włamano się”
  • Nie można użyć bibliotek z powodu małego problemu (błąd, brak funkcjonalności, „niewłaściwe” zachowanie), którego nie można zmienić z powodu ograniczonej widoczności
  • Problem, którego nie można naprawić, doprowadził do ogromnych, brzydkich i błędnych obejść, w których zastąpienie prostej funkcji (prywatnej lub ostatecznej) mogło pomóc

Zacząłem je nazywać, dopóki pytanie nie stało się zbyt długie, i postanowiłem je usunąć.

Podoba mi się pomysł, aby nie mieć więcej kodu niż potrzeba, więcej widoczności niż potrzeba, więcej abstrakcji niż potrzeba. Może to działać podczas pisania aplikacji dla użytkownika końcowego, gdzie kod jest używany tylko przez tych, którzy go piszą. Ale jak to się utrzymuje, jeśli kod ma być używany przez innych programistów, gdzie jest nieprawdopodobne, aby pierwotny programista z góry pomyślał o każdym możliwym przypadku użycia, a zmiany / refaktory są trudne / niemożliwe do wprowadzenia?

Ponieważ duże biblioteki open source nie są niczym nowym, jaki jest najczęstszy sposób obsługi widoczności w takich projektach z językami obiektowymi?

piegry
źródło
biorąc pod uwagę, że pytasz o open source, naginanie odpowiednich zasad kodowania w celu rozwiązania wymienionych problemów jest mniej sensowne niż po prostu zamknięte źródło, po prostu dlatego, że można wprowadzić potrzebne poprawki bezpośrednio do kodu biblioteki lub rozwidlić go i stworzyć własną wersję z cokolwiek korekty chcą
komara
2
nie chodzi mi o to, ale o to, że odniesienie do open source nie ma sensu w tym kontekście. Mogę sobie wyobrazić, jak pragmatyczne potrzeby mogą uzasadniać odstępstwo od ścisłych zasad w niektórych przypadkach (znane również jako narastanie długu technicznego ), ale z tej perspektywy nie ma znaczenia, czy kod jest zamknięty, czy otwarty. A ściślej mówiąc, ma to znaczenie w przeciwnym kierunku, niż sobie wyobrażałeś, ponieważ kod będący otwartym oprogramowaniem może sprawić, że te potrzeby będą mniej pilne niż zamknięte, ponieważ oferuje dodatkowe opcje rozwiązania tych problemów
gnat
1
@piegames: W pełni zgadzam się na komentowanie tutaj, problemy, które naszkicowałeś, są o wiele bardziej prawdopodobne w bibliotekach zamkniętych z kodem źródłowym - jeśli jest to biblioteka systemowa z licencją zezwalającą, jeśli opiekunowie ignorują żądanie zmiany, można rozwidlić bibliotekę i w razie potrzeby zmień widoczność samodzielnie.
Doc Brown
1
@piegames: Nie rozumiem twojego pytania. „Java” to język, a nie lib. A jeśli „Twoja mała biblioteka open source” ma zbyt ścisłą widoczność, późniejsze zwiększenie widoczności zwykle nie narusza wstecznej kompatybilności. Tylko na odwrót.
Doc Brown

Odpowiedzi:

15

Niefortunna prawda jest taka, że ​​wiele bibliotek jest pisanych , a nie projektowanych . To smutne, ponieważ odrobina wcześniejszych przemyśleń może zapobiec wielu problemom na drodze.

Jeśli postanowimy zaprojektować bibliotekę, pojawi się pewien zestaw przewidywanych przypadków użycia. Biblioteka może nie spełniać bezpośrednio wszystkich przypadków użycia, ale może służyć jako część rozwiązania. Dlatego biblioteka musi być wystarczająco elastyczna, aby się przystosować.

Ograniczeniem jest to, że zazwyczaj nie jest dobrym pomysłem pobranie kodu źródłowego biblioteki i zmodyfikowanie go w celu obsługi nowego przypadku użycia. W przypadku bibliotek zastrzeżonych źródło może nie być dostępne, aw przypadku bibliotek typu open source może być niepożądane utrzymanie wersji rozwidlonej. Połączenie wysoce specyficznych dostosowań w projekcie wyższego szczebla może być niewykonalne.

Tutaj pojawia się zasada otwartego zamknięcia: biblioteka powinna być otwarta na rozszerzenie bez modyfikowania kodu źródłowego. To nie przychodzi naturalnie. To musi być celowy cel projektu. Istnieje wiele technik, które mogą tu pomóc, klasyczne wzorce projektowania OOP to tylko niektóre z nich. Ogólnie rzecz biorąc, określamy zaczepy, w których kod użytkownika może bezpiecznie podłączyć się do biblioteki i dodać funkcjonalność.

Po prostu upublicznienie każdej metody lub zezwolenie na podklasę każdej klasy nie jest wystarczające do osiągnięcia rozszerzalności. Po pierwsze, rozszerzenie biblioteki jest naprawdę trudne, jeśli nie jest jasne, gdzie użytkownik może podłączyć się do biblioteki. Np. Przesłonięcie większości metod nie jest bezpieczne, ponieważ metoda klasy bazowej została napisana z niejawnymi założeniami. Naprawdę musisz zaprojektować dla rozszerzenia.

Co ważniejsze, gdy coś jest częścią publicznego interfejsu API, nie można go odzyskać. Nie można go refaktoryzować bez zerwania kodu dalszego przetwarzania. Przedwczesna otwartość ogranicza bibliotekę do nieoptymalnego projektu. W przeciwieństwie do tego, uczynienie wewnętrznych rzeczy prywatnymi, ale dodawanie haków, jeśli później będą potrzebne, jest bezpieczniejszym podejściem. Mimo, że jest to sposób rozsądny zająć się ewolucją długoterminową biblioteki, jest niezadowalająca dla użytkowników, którzy chcą korzystać z biblioteki teraz .

Co zatem się dzieje? Jeśli obecny stan biblioteki powoduje znaczny ból, programiści mogą wziąć całą wiedzę na temat faktycznych przypadków użycia, które nagromadziły się w czasie, i napisać wersję 2 biblioteki. Będzie świetnie! Naprawi to wszystkie błędy związane z projektowaniem! Zajmie to również więcej czasu, niż się spodziewano, w wielu przypadkach zgasło. A jeśli nowa wersja jest bardzo odmienna od starej, może być trudno zachęcić użytkowników do migracji. Pozostajesz wtedy utrzymywać dwie niekompatybilne wersje.

amon
źródło
Muszę więc dodać haczyki do rozszerzenia, ponieważ samo upublicznienie / zastąpienie nie wystarczy. Muszę też zastanowić się, kiedy opublikować zmiany / nowy interfejs API ze względu na kompatybilność wsteczną. Ale co z widocznością metod specjalnych?
piegames
@piegames Dzięki widoczności decydujesz, które części są publiczne (część stabilnego interfejsu API), a które części prywatne (mogą ulec zmianie). Jeśli ktoś obejdzie to przez odbicie, jest to ich problem, gdy ta funkcja zepsuje się w przyszłości. Nawiasem mówiąc, punkty rozszerzenia są często w formie metody, którą można zastąpić. Istnieje jednak różnica między metodą, którą można zastąpić, a metodą, która ma zostać zastąpiona (patrz także Wzorzec metody szablonu).
amon
8

Każda publiczna i rozszerzalna klasa / metoda jest częścią interfejsu API, który musi być obsługiwany. Ograniczenie tego zestawu do rozsądnego podzbioru biblioteki zapewnia największą stabilność i ogranicza liczbę rzeczy, które mogą się nie udać. Jest to decyzja zarządcza (a nawet projekty OSS są zarządzane w pewnym stopniu) na podstawie tego, co możesz w uzasadniony sposób wesprzeć.

Różnica między OSS a zamkniętym źródłem polega na tym, że większość ludzi próbuje stworzyć i rozwijać społeczność wokół kodu, tak aby więcej niż jedna osoba obsługiwała bibliotekę. To powiedziawszy, dostępnych jest wiele narzędzi do zarządzania:

  • Listy mailingowe omawiają potrzeby użytkowników i sposoby implementacji
  • Systemy śledzenia problemów (problemy JIRA lub Git itp.) Śledzą błędy i żądania funkcji
  • Kontrola wersji zarządza kodem źródłowym.

W dojrzałych projektach zobaczysz coś takiego:

  1. Ktoś chce zrobić coś z biblioteką, do której pierwotnie nie została zaprojektowana
  2. Dodają bilet do śledzenia problemów
  3. Zespół może omawiać problem na liście mailingowej lub w komentarzach, a wnioskodawca jest zawsze zapraszany do przyłączenia się do dyskusji
  4. Z jakiegoś powodu zmiana interfejsu API jest akceptowana i nadawana priorytetowi lub odrzucana

W tym momencie, jeśli zmiana została zaakceptowana, ale użytkownik chce przyspieszyć jej naprawę, może wykonać pracę i przesłać żądanie ściągnięcia lub łatkę (w zależności od narzędzia kontroli wersji).

Żadne API nie jest statyczne. Jednak jego rozwój musi być w jakiś sposób ukształtowany. Trzymając wszystko zamknięte, dopóki nie zostanie wykazana potrzeba otwarcia rzeczy, unikniesz reputacji błędnej lub niestabilnej biblioteki.

Berin Loritsch
źródło
1
W pełni się zgadzam i z powodzeniem przećwiczyłem proces żądania zmiany, który nakreśliłeś dla bibliotek stron trzecich z zamkniętym kodem źródłowym, a także dla bibliotek typu open source.
Doc Brown
Z mojego doświadczenia wynika, że ​​nawet niewielkie zmiany w małych bibliotekach są bardzo pracochłonne (nawet jeśli mają to tylko na celu przekonanie innych) i mogą zająć trochę czasu (czekać na następne wydanie, jeśli nie możesz sobie pozwolić na użycie migawki do tego czasu). Więc to oczywiście nie jest dla mnie opcja. Byłbym jednak zainteresowany: czy są jakieś większe biblioteki (w GitHub), które naprawdę używają tej koncepcji?
piegames
To zawsze dużo pracy. Prawie każdy projekt, do którego się przyczyniłem, ma podobny proces. W czasach Apache mogliśmy dyskutować przez kilka dni, ponieważ pasjonowaliśmy się tym, co stworzyliśmy. Wiedzieliśmy, że wiele osób będzie korzystać z bibliotek, więc musieliśmy przedyskutować takie rzeczy, jak to, czy proponowana zmiana zamierza złamać API, czy proponowana funkcja jest tego warta, kiedy to zrobić? itd.
Berin Loritsch
0

Przeredaguję moją odpowiedź, ponieważ wydaje się, że uderzyła ona w nerwy kilku osobom.

widoczność właściwości / metody klasy nie ma nic wspólnego z bezpieczeństwem ani otwartością źródła.

Powodem istnienia widoczności jest to, że obiekty są wrażliwe na 4 konkretne problemy:

  1. konkurencja

Jeśli zbudujesz moduł nieskapsułowany, użytkownicy przyzwyczają się do bezpośredniej zmiany stanu modułu. Działa to dobrze w środowisku z jednym wątkiem, ale kiedy nawet pomyślisz o dodaniu wątków; będziesz zmuszony uczynić państwo prywatnym i używać blokad / monitorów wraz z modułami pobierającymi i ustawiającymi, które zmuszają inne wątki do oczekiwania na zasoby, zamiast ścigać się na nich. Oznacza to, że programy użytkowników nie będą już działać, ponieważ do zmiennych prywatnych nie można uzyskać dostępu w tradycyjny sposób. Może to oznaczać, że potrzebujesz dużo przepisywania.

Prawda jest taka, że ​​o wiele łatwiej jest kodować przy użyciu jednego wątkowego środowiska uruchomieniowego, a prywatne słowo kluczowe pozwala po prostu dodać słowo kluczowe zsynchronizowane lub kilka blokad, a kod użytkowników nie złamie się, jeśli zostanie zamknięty w sobie od początku .

  1. Pomóż zapobiegać strzelaniu do siebie przez użytkowników podczas korzystania z interfejsu. Zasadniczo pomaga kontrolować niezmienniki obiektu.

Każdy obiekt ma wiele rzeczy, które muszą być prawdziwe, aby być w stanie spójnym. Niestety, te rzeczy żyją w widocznej przestrzeni klienta, ponieważ przenoszenie każdego obiektu do własnego procesu i rozmawianie z nim za pomocą wiadomości jest drogie. Oznacza to, że obiektowi bardzo łatwo jest zawiesić cały program, jeśli użytkownik ma pełną widoczność.

Jest to nieuniknione, ale można zapobiec przypadkowemu wprowadzeniu obiektu w niespójny stan, zamykając interfejs w jego usługach, co zapobiega przypadkowym awariom, pozwalając tylko użytkownikowi na interakcję ze stanem obiektu poprzez starannie spreparowany interfejs, który czyni program o wiele bardziej niezawodnym . Nie oznacza to, że użytkownik nie może celowo uszkodzić niezmienników, ale jeśli tak, to ich klient ulega awarii, wystarczy, że uruchomi ponownie program (dane, które chcesz chronić, nie powinny być przechowywane po stronie klienta) ).

Innym dobrym przykładem, w którym można poprawić użyteczność modułów, jest ustawienie konstruktora na prywatny; ponieważ jeśli konstruktor zgłosi wyjątek, zabije program. Jednym leniwym podejściem do rozwiązania tego problemu jest spowodowanie przez konstruktora błędu czasu kompilacji, którego nie można zbudować, chyba że znajduje się on w bloku try / catch. Ustawiając konstruktor na prywatny i dodając publiczną statyczną metodę tworzenia, można ustawić, aby metoda tworzenia zwróciła null, jeśli nie uda się jej zbudować, lub weź funkcję wywołania zwrotnego do obsługi błędu, dzięki czemu program będzie bardziej przyjazny dla użytkownika.

  1. Zakres zanieczyszczenia

Wiele klas ma wiele stanów i metod i łatwo jest zostać przytłoczonym, próbując je przewijać; Wiele z tych metod to po prostu szum wizualny, taki jak funkcje pomocnicze, stan. ustawienie zmiennych i metod na prywatne pomaga zmniejszyć zanieczyszczenie zakresem i ułatwia użytkownikowi znalezienie usług, których szukają.

Zasadniczo pozwala ci uniknąć funkcji pomocniczych wewnątrz klasy, a nie poza nią; bez kontroli widoczności bez rozpraszania użytkownika szeregiem usług, z których użytkownik nigdy nie powinien korzystać, dzięki czemu można uniknąć podziału metod na kilka metod pomocniczych (choć nadal będzie to zanieczyszczało zakres, ale nie użytkownika).

  1. związany z zależnościami

Dobrze spreparowany interfejs może ukryć wewnętrzne bazy danych / okna / obrazowanie, od których zależy jego wykonanie, a jeśli chcesz przejść do innej bazy danych / innego systemu okien / innej biblioteki obrazowania, możesz zachować ten sam interfejs i użytkowników nie zauważy.

Z drugiej strony, jeśli tego nie zrobisz, możesz łatwo wpaść w niemożność zmiany swoich zależności, ponieważ są one ujawnione, a kod na tym polega. Przy wystarczająco dużym systemie koszt migracji może stać się nieosiągalny, a jego enkapsulacja może chronić dobrze zachowujących się użytkowników klientów przed przyszłymi decyzjami o zamianie zależności.

Dmitry
źródło
1
„Nie ma sensu niczego ukrywać” - to po co w ogóle myśleć o enkapsulacji? W wielu kontekstach refleksja wymaga specjalnych uprawnień.
Frank Hileman
Myślisz o enkapsulacji, ponieważ daje ci ona oddech podczas opracowywania modułu i zmniejsza prawdopodobieństwo niewłaściwego użycia. Na przykład, jeśli masz 4 wątki bezpośrednio modyfikujące stan wewnętrzny klasy, z łatwością spowoduje to problemy, a uczynienie zmiennej prywatną, zachęca użytkownika do korzystania z publicznych metod do manipulowania stanem świata, które mogą używać monitorów / blokad, aby zapobiegać problemom . Jest to jedyna prawdziwa zaleta kapsułkowania.
Dmitry
Ukrywanie rzeczy ze względów bezpieczeństwa jest łatwym sposobem na zaprojektowanie projektu, w którym skończysz z dziurami w interfejsie API. Dobrym przykładem tego są aplikacje do obsługi wielu dokumentów, w których jest wiele zestawów narzędzi i wiele okien z oknami podrzędnymi. Jeśli zwariujesz na punkcie kapsułkowania, skończy się sytuacja, w której narysujesz coś na jednym dokumencie, musisz poprosić okno, aby poprosiło wewnętrzny dokument, aby poprosił wewnętrzny dokument, aby poprosił wewnętrzny dokument, aby poprosił o narysowanie czegoś i unieważnij jego kontekst. Jeśli strona klienta chce grać po stronie klienta, nie możesz temu zapobiec.
Dmitry
OK, to ma większy sens, chociaż bezpieczeństwo można osiągnąć poprzez kontrolę dostępu, jeśli środowisko to obsługuje, i był to jeden z pierwotnych celów w projektowaniu języka OO. Promujesz także enkapsulację i mówisz, że nie używaj jej w tym samym czasie; trochę mylące.
Frank Hileman
Nigdy nie chciałem tego nie używać; Miałem na myśli, że nie używaj tego ze względów bezpieczeństwa; użyj go strategicznie, aby poprawić wrażenia użytkowników i zapewnić płynniejsze środowisko programistyczne. Chodzi mi o to, że nie ma to nic wspólnego z bezpieczeństwem ani otwartością źródła. Obiekty po stronie klienta są z definicji podatne na introspekcję, a przeniesienie ich poza przestrzeń procesową użytkownika sprawia, że ​​rzeczy nieskapsułowane są równie niedostępne, co kapsułkowane.
Dmitry