Podczas czytania różnych pytań o przepełnienie stosu i kodu innych osób ogólny konsensus dotyczący projektowania klas jest zamknięty. Oznacza to, że domyślnie w Javie i C # wszystko jest prywatne, pola są ostateczne, niektóre metody są ostateczne, a czasem klasy są nawet ostateczne .
Chodzi o to, aby ukryć szczegóły implementacji, co jest bardzo dobrym powodem. Jednak protected
w przypadku większości języków OOP i polimorfizmu nie działa to.
Za każdym razem, gdy chcę dodać lub zmienić funkcjonalność klasy, zwykle przeszkadzają mi prywatne i końcowe miejsca. Tutaj ważne są szczegóły implementacji: bierzesz implementację i ją rozszerzasz, dobrze wiesz, jakie są konsekwencje. Ponieważ jednak nie mogę uzyskać dostępu do prywatnych i końcowych pól i metod, mam trzy opcje:
- Nie rozszerzaj klasy, po prostu obejdź problem prowadzący do bardziej złożonego kodu
- Skopiuj i wklej całą klasę, zabijając wielokrotnego użytku kod
- Rozwidlaj projekt
To nie są dobre opcje. Dlaczego nie jest protected
stosowany w projektach napisanych w językach, które go obsługują? Dlaczego niektóre projekty wyraźnie zabraniają dziedziczenia po swoich klasach?
źródło
Odpowiedzi:
Projektowanie klas, aby działały poprawnie po rozszerzeniu, szczególnie gdy programista wykonujący rozszerzenie nie do końca rozumie, jak ma działać klasa, wymaga znacznego dodatkowego wysiłku. Nie możesz po prostu wziąć wszystkiego, co prywatne, upublicznić (lub chronić) i nazwać to „otwartym”. Jeśli pozwalasz komuś innemu na zmianę wartości zmiennej, musisz rozważyć, w jaki sposób wszystkie możliwe wartości wpłyną na klasę. (Co jeśli ustawią zmienną na null? Pusta tablica? Liczba ujemna?) To samo dotyczy przyzwalania innym osobom na wywoływanie metody. To wymaga dokładnego przemyślenia.
Nie chodzi więc o to, że klasy nie powinny być otwarte, ale czasami nie warto starać się, aby były one otwarte.
Oczywiście możliwe jest również, że autorzy bibliotek byli po prostu leniwi. Zależy od biblioteki, o której mówisz. :-)
źródło
Uczynienie wszystkiego domyślnym prywatnym brzmi surowo, ale spójrz na to z drugiej strony: Gdy wszystko jest domyślnie prywatne, uczynienie czegoś publicznym (lub chronionym, co jest prawie tym samym) powinno być świadomym wyborem; jest to umowa autora klasy z tobą, konsumentem, na temat korzystania z klasy. Jest to wygoda dla was obojga: autor może modyfikować wewnętrzne funkcjonowanie klasy, o ile interfejs pozostaje niezmieniony; i dokładnie wiesz, na których częściach klasy możesz polegać, a które mogą ulec zmianie.
Podstawową ideą jest „luźne sprzężenie” (zwane również „wąskimi interfejsami”); jego wartość polega na utrzymaniu złożoności na niskim poziomie. Zmniejszając liczbę sposobów interakcji komponentów, zmniejsza się również wzajemna zależność między nimi; a współzależność jest jednym z najgorszych rodzajów złożoności, jeśli chodzi o utrzymanie i zarządzanie zmianami.
W dobrze zaprojektowanych bibliotekach klasy, które warto rozszerzyć poprzez dziedziczenie, chroniły członków publicznych w odpowiednich miejscach i ukrywają wszystko inne.
źródło
Wszystko, co nie jest prywatne, powinno istnieć mniej więcej w niezmienionym zachowaniu w każdej przyszłej wersji klasy. W rzeczywistości można to uznać za część interfejsu API, udokumentowane lub nie. Dlatego ujawnienie zbyt wielu szczegółów prawdopodobnie spowoduje później problemy ze zgodnością.
W odniesieniu do „ostatecznego” wzgl. klasy „zapieczętowane”, jednym typowym przypadkiem są klasy niezmienne. Wiele części frameworka zależy od niezmienności łańcuchów. Gdyby klasa String nie była ostateczna, byłoby łatwo (nawet kuszące) stworzyć zmienną podklasę String, która powoduje różnego rodzaju błędy w wielu innych klasach, gdy jest używana zamiast niezmiennej klasy String.
źródło
final
i / lubprivate
jest zablokowane i nigdy nie może zostać zmienione .public
członków;protected
członkowie zawierają umowę tylko między klasą, która je ujawnia, a jej bezpośrednimi klasami pochodnymi; dla kontrastupublic
członkowie kontraktów ze wszystkimi konsumentami w imieniu wszystkich możliwych obecnych i przyszłych klas dziedziczonych.W OO istnieją dwa sposoby na dodanie funkcjonalności do istniejącego kodu.
Pierwszy to dziedziczenie: bierzesz klasę i czerpiesz z niej. Dziedziczenia należy jednak używać z rozwagą. Powinieneś używać dziedziczenia publicznego głównie wtedy, gdy masz relację isA między bazą a klasą pochodną (np. Prostokąt jest kształtem ). Zamiast tego należy unikać publicznego dziedziczenia za ponowne użycie istniejącej implementacji. Zasadniczo dziedziczenie publiczne służy do upewnienia się, że klasa pochodna ma taki sam interfejs jak klasa podstawowa (lub większa), dzięki czemu można zastosować zasadę podstawienia Liskowa .
Inną metodą dodania funkcjonalności jest użycie delegacji lub kompozycji. Piszesz własną klasę, która wykorzystuje istniejącą klasę jako obiekt wewnętrzny i deleguje do niej część prac związanych z implementacją.
Nie jestem pewien, jaki był pomysł na te wszystkie finały w bibliotece, z której próbujesz skorzystać. Prawdopodobnie programiści chcieli się upewnić, że nie odziedziczysz po nich nowej klasy, ponieważ nie jest to najlepszy sposób na rozszerzenie ich kodu.
źródło
Większość odpowiedzi ma rację: klasy powinny być zapieczętowane (ostateczne, nieprzekraczalne itp.), Gdy obiekt nie jest zaprojektowany ani przeznaczony do rozszerzenia.
Jednak nie rozważałbym po prostu zamknięcia wszystkiego właściwego projektu kodu. „O” w SOLID oznacza „zasadę otwartego-zamkniętego”, mówiącą, że klasa powinna być „zamknięta” dla modyfikacji, ale „otwarta” dla rozszerzenia. Chodzi o to, że masz kod w obiekcie. Działa dobrze. Dodanie funkcjonalności nie powinno wymagać otwarcia tego kodu i wprowadzenia zmian chirurgicznych, które mogą przerwać wcześniej działające zachowanie. Zamiast tego należy zastosować zastrzyk dziedziczenia lub zależności, aby „rozszerzyć” klasę na dodatkowe czynności.
Na przykład klasa „ConsoleWriter” może pobrać tekst i wysłać go do konsoli. Robi to dobrze. Jednak w pewnym przypadku potrzebujesz również tego samego wyjścia zapisanego w pliku. Otwarcie kodu ConsoleWriter, zmiana jego zewnętrznego interfejsu w celu dodania parametru „WriteToFile” do głównych funkcji oraz umieszczenie dodatkowego kodu do zapisywania plików obok kodu do pisania w konsoli byłoby ogólnie uważane za coś złego.
Zamiast tego możesz zrobić jedną z dwóch rzeczy: możesz wywodzić się z ConsoleWriter w celu utworzenia ConsoleAndFileWriter i rozszerzyć metodę Write (), aby najpierw wywołać implementację podstawową, a następnie zapisać do pliku. Możesz też wyodrębnić interfejs IWriter z ConsoleWriter i ponownie wdrożyć ten interfejs, aby utworzyć dwie nowe klasy; FileWriter i „MultiWriter”, które mogą otrzymać inne IWriters i będą „nadawać” każde wywołanie jego metod do wszystkich podanych pisarzy. Który wybierzesz zależy od tego, czego będziesz potrzebować; proste wyprowadzenie jest, cóż, prostsze, ale jeśli masz niejasne przewidywanie konieczności wysłania wiadomości do klienta sieciowego, trzech plików, dwóch nazwanych potoków i konsoli, śmiało spróbuj wyodrębnić interfejs i utworzyć „Adapter Y”; to'
Teraz, jeśli funkcja Write () nigdy nie została zadeklarowana jako wirtualna (lub została zapieczętowana), teraz masz kłopoty, jeśli nie kontrolujesz kodu źródłowego. Czasami nawet jeśli tak. Na ogół jest to zła sytuacja i frustruje użytkowników interfejsów API o zamkniętym lub ograniczonym źródle bez końca. Ale istnieje uzasadniony powód, dla którego Write () lub cała klasa ConsoleWriter są zapieczętowane; w chronionym polu mogą znajdować się wrażliwe informacje (zastępowane kolejno z klasy podstawowej w celu zapewnienia wspomnianych informacji). Walidacja może być niewystarczająca; kiedy piszesz interfejs API, musisz założyć, że programiści korzystający z twojego kodu nie są mądrzejsi ani życzliwsi niż przeciętny „użytkownik końcowy” ostatecznego systemu; w ten sposób nigdy nie będziesz rozczarowany.
źródło
Sądzę, że prawdopodobnie pochodzą od wszystkich osób używających języka oo do programowania c. Patrzę na ciebie, java. Jeśli twój młot ma kształt obiektu, ale chcesz system proceduralny i / lub tak naprawdę nie rozumiesz klas, wtedy twój projekt będzie musiał zostać zamknięty.
Zasadniczo, jeśli zamierzasz pisać w języku oo, Twój projekt musi być zgodny z paradygmatem oo, który obejmuje rozszerzenie. Oczywiście, jeśli ktoś napisał bibliotekę w innym paradygmacie, być może nie powinieneś i tak go rozszerzać.
źródło
Ponieważ nie wiedzą nic lepszego.
Oryginalni autorzy prawdopodobnie trzymają się niezrozumienia zasad SOLID, które powstały w zagmatwanym i skomplikowanym świecie C ++.
Mam nadzieję, że zauważysz, że światy ruby, python i perl nie mają problemów, które tutaj twierdzą, że są przyczyną uszczelnienia. Zauważ, że jest to pisanie prostopadłe do dynamicznego. Modyfikatory dostępu są łatwe do pracy w większości (wszystkich?) Językach. Pola C ++ można zmulować rzutując na inny typ (C ++ jest bardziej słaby). Java i C # mogą używać odbicia. Modyfikatory dostępu sprawiają, że wszystko jest na tyle trudne, że nie możesz tego zrobić, chyba że NAPRAWDĘ chcesz.
Pieczętowanie klas i oznaczanie dowolnych członków jawnie narusza zasadę, że proste rzeczy powinny być proste, a trudne rzeczy powinny być możliwe. Nagle rzeczy, które powinny być proste, nie są.
Zachęcam do spróbowania zrozumieć punkt widzenia oryginalnych autorów. Wiele z nich pochodzi z akademickiej idei kapsułkowania, która nigdy nie pokazała absolutnego sukcesu w prawdziwym świecie. Nigdy nie widziałem frameworka ani biblioteki, w której gdzieś programista nie chciałby, aby działał nieco inaczej i nie miał dobrego powodu, aby go zmieniać. Istnieją dwie możliwości, które mogły nękać pierwotnych twórców oprogramowania, którzy przypieczętowali i uczynili członków prywatnymi.
Wydaje mi się, że w korporacyjnym frameworku nr 2 prawdopodobnie tak jest. Te frameworki C ++, Java i .NET muszą zostać „wykonane” i muszą przestrzegać określonych wytycznych. Wytyczne te zwykle oznaczają zapieczętowane typy, chyba że typ został wyraźnie zaprojektowany jako część hierarchii typów i członków prywatnych dla wielu rzeczy, które mogą być przydatne dla innych osób .. ale ponieważ nie odnoszą się bezpośrednio do tej klasy, nie są narażone . Wyodrębnienie nowego typu byłoby zbyt kosztowne do obsługi, dokumentowania itp.
Cała idea modyfikatorów dostępu polega na tym, że programiści powinni być chronieni przed sobą. „Programowanie C jest złe, ponieważ pozwala strzelać sobie w stopę”. Jako programista nie zgadzam się z tą filozofią.
Zdecydowanie wolę podejście manglingujące nazwy pytona. W razie potrzeby możesz łatwo (znacznie łatwiej niż refleksja) wymienić szeregowców. Świetny opis na ten temat jest dostępny tutaj: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/
Prywatny modyfikator Ruby jest właściwie bardziej chroniony w C # i nie ma prywatnego jako modyfikatora C #. Protected jest trochę inny. Świetne wyjaśnienie tutaj: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/
Pamiętaj, że Twój język statyczny nie musi być zgodny ze starymi stylami programu do pisania kodów w tamtej przeszłości.
źródło
EDYCJA: Uważam, że klasy powinny być zaprojektowane tak, aby były otwarte. Z pewnymi ograniczeniami, ale nie należy ich zamykać na dziedziczenie.
Dlaczego: „Klasy końcowe” lub „klasy zamknięte” wydają mi się dziwne. Nigdy nie muszę oznaczać jednej z moich klas jako „końcowej” („zapieczętowanej”), ponieważ być może później będę musiał ją dziedziczyć.
Kupuję / pobieram biblioteki stron trzecich z klasami (większość kontroli wizualnych) i naprawdę jestem wdzięczny, że żadna z tych bibliotek nie używała „wersji ostatecznej”, nawet jeśli jest obsługiwana przez język programowania, w którym zostały zakodowane, ponieważ zakończyłem rozszerzanie tych klas, nawet jeśli skompilowałem biblioteki bez kodu źródłowego.
Czasami mam do czynienia z niektórymi okazjonalnymi „zapieczętowanymi” klasami i kończyłem tworzenie nowego „opakowania” zawierającego daną klasę, która ma podobnych członków.
Klasyfikatory zakresu pozwalają na „uszczelnienie” niektórych funkcji bez ograniczania dziedziczenia.
Kiedy przełączyłem się z Structured Progr. do OOP zacząłem używać członków klasy prywatnej , ale po wielu projektach przestałem używać chronionych , a ostatecznie promować te właściwości lub metody publicznie , jeśli to konieczne.
PS Nie lubię słowa „ostateczny” jako słowa kluczowego w języku C #, wolę raczej używać „zapieczętowanego” jak Java lub „sterylnego”, zgodnie z metaforą dziedziczenia. Poza tym „końcowy” to słowo niejednoznaczne używane w kilku kontekstach.
źródło