Po ponad 10 latach programowania w języku Java / C # zaczynam tworzyć:
- klasy abstrakcyjne : umowa nie ma być tworzona jako taka, jaka jest.
- klasy końcowe / zapieczętowane : implementacja nie ma służyć jako klasa bazowa dla czegoś innego.
Nie mogę wymyślić żadnej sytuacji, w której prosta „klasa” (tj. Ani abstrakcyjna, ani ostateczna / zapieczętowana) byłaby „mądrym programowaniem”.
Dlaczego klasa powinna być czymś innym niż „abstrakcyjnym” lub „ostatecznym / zapieczętowanym”?
EDYTOWAĆ
Ten świetny artykuł wyjaśnia moje obawy znacznie lepiej niż potrafię.
object-oriented
programming-practices
Nicolas Repiquet
źródło
źródło
Open/Closed principle
, a nieClosed Principle.
Odpowiedzi:
Jak na ironię, uważam, że jest odwrotnie: użycie klas abstrakcyjnych jest raczej wyjątkiem niż regułą i mam skłonność do marszczenia brwi na końcowych / zapieczętowanych klasach.
Interfejsy są bardziej typowym mechanizmem według umowy, ponieważ nie określasz żadnych elementów wewnętrznych - nie martwisz się o nie. Pozwala to na niezależność każdej realizacji tego kontraktu. Jest to kluczowe w wielu domenach. Na przykład, jeśli budujesz ORM, bardzo ważne jest, abyś mógł przekazać zapytanie do bazy danych w jednolity sposób, ale implementacje mogą być zupełnie inne. Jeśli użyjesz do tego celu klas abstrakcyjnych, ostatecznie okablujesz elementy, które mogą, ale nie muszą mieć zastosowania do wszystkich implementacji.
W przypadku klas końcowych / zapieczętowanych jedyną wymówką, jaką kiedykolwiek widzę, by z nich korzystać, jest to, że zezwolenie na zastąpienie jest w rzeczywistości niebezpieczne - być może algorytm szyfrowania lub coś takiego. Poza tym nigdy nie wiesz, kiedy możesz chcieć rozszerzyć funkcjonalność z przyczyn lokalnych. Pieczętowanie klasy ogranicza twoje opcje dla zysków, które nie istnieją w większości scenariuszy. O wiele bardziej elastyczne jest pisanie klas w taki sposób, aby można je było później rozszerzyć.
Ten ostatni pogląd został utrwalony przez pracę z komponentami innych firm, które uszczelniały klasy, uniemożliwiając w ten sposób integrację, która znacznie ułatwiłaby życie.
źródło
Że jest to świetny artykuł Eric Lippert, ale nie sądzę, że obsługuje swój punkt widzenia.
Twierdzi, że wszystkie klasy wystawione do użytku przez innych powinny być zapieczętowane lub w inny sposób niemożliwe do rozszerzenia.
Twoim założeniem jest, że wszystkie klasy powinny być abstrakcyjne lub zapieczętowane.
Duża różnica.
Artykuł EL nic nie mówi o (prawdopodobnie wielu) klasach produkowanych przez jego zespół, o których ty i ja nic nie wiemy. Ogólnie rzecz biorąc, publicznie wyeksponowane klasy w ramach są tylko podzbiorem wszystkich klas zaangażowanych we wdrażanie tych ram.
źródło
new List<Foo>()
czymś takimList<Foo>.CreateMutable()
.Z perspektywy Java uważam, że końcowe klasy nie są tak mądre, jak się wydaje.
Wiele narzędzi (zwłaszcza AOP, JPA itp.) Działa z opóźnieniem czasu ładowania, więc muszą rozszerzyć twoje klauzule. Innym sposobem byłoby utworzenie delegatów (nie tych .NET) i delegowanie wszystkiego do oryginalnej klasy, co byłoby znacznie bardziej nieporządne niż rozszerzenie klas użytkowników.
źródło
Dwa typowe przypadki, w których będziesz potrzebować waniliowych, niezapieczętowanych klas:
Techniczne: jeśli masz hierarchię głęboką na więcej niż dwa poziomy i chcesz mieć możliwość tworzenia czegoś pośrodku.
Zasada: Czasami pożądane jest pisanie klas, które są jawne, zaprojektowane w taki sposób, aby były bezpiecznie rozszerzane . (Dzieje się tak często, gdy piszesz API takie jak Eric Lippert lub pracujesz w zespole przy dużym projekcie). Czasami chcesz napisać klasę, która działa sama, ale jest zaprojektowana z myślą o rozszerzalności.
Eric Lippert myśli dotyczące marek uszczelniających sens, ale przyznaje też, że oni zrobić projekt na rozciągliwość pozostawiając klasie „open”.
Tak, wiele klas jest zaplombowanych w BCL, ale ogromna liczba klas nie jest i można je rozszerzać na wiele wspaniałych sposobów. Jednym z przykładów, który przychodzi na myśl, są formularze Windows Forms, w których można dodawać dane lub zachowania do prawie każdego
Control
poprzez dziedziczenie. Oczywiście można to zrobić na inne sposoby (wzór dekoratora, różne rodzaje kompozycji itp.), Ale dziedziczenie również działa bardzo dobrze.Dwie uwagi dotyczące .NET:
virtual
niefunkcjonalnością, z wyjątkiem jawnych implementacji interfejsu.internal
zamiast uszczelnienia klasy, co pozwala na odziedziczenie go w bazie kodu, ale nie poza nim.źródło
Uważam, że prawdziwym powodem, dla którego wielu ludzi uważa, że klasy powinny być
final
/sealed
jest to, że większość nieabstrakcyjnych rozszerzalnych klas nie jest odpowiednio dokumentowana .Pozwól mi rozwinąć. Zaczynając z daleka, niektórzy programiści uważają, że dziedziczenie jako narzędzie w OOP jest powszechnie nadużywane i nadużywane. Wszyscy przeczytaliśmy zasadę substytucji Liskowa, choć nie powstrzymało nas to przed naruszeniem jej setki (może nawet tysiące) razy.
Chodzi o to, że programiści uwielbiają ponownie wykorzystywać kod. Nawet jeśli nie jest to dobry pomysł. Dziedziczenie jest kluczowym narzędziem w tym „ponownym wykorzystywaniu” kodu. Powrót do ostatniego / zamkniętego pytania.
Właściwa dokumentacja dla klasy końcowej / zapieczętowanej jest stosunkowo niewielka: opisujesz, co robi każda metoda, jakie są argumenty, zwracana wartość itp. Wszystkie zwykłe rzeczy.
Jednak, gdy odpowiednio dokumentujesz rozszerzalną klasę , musisz uwzględnić przynajmniej następujące elementy :
super
implementację? Czy wywołujesz ją na początku metody, czy na końcu? Pomyśl konstruktor vs destruktor)Są tuż przy mojej głowie. Mogę ci podać przykład, dlaczego każdy z nich jest ważny, a pominięcie go zepsuje rozszerzającą się klasę.
Teraz zastanów się, ile wysiłku w dokumentację należy poświęcić na odpowiednie udokumentowanie każdej z tych rzeczy. Uważam, że klasa z 7-8 metodami (która może być za dużo w wyidealizowanym świecie, ale w rzeczywistości jest za mało) równie dobrze mogłaby mieć dokumentację o 5 stronach, tylko tekstową. Zamiast tego wychylamy się do połowy i nie zamykamy klasy, aby inni mogli z niej korzystać, ale też nie dokumentowali jej odpowiednio, ponieważ zajmie to ogromną ilość czasu (i wiesz, że może i tak nigdy nie będzie przedłużany, więc po co zawracać sobie głowę?).
Jeśli projektujesz klasę, możesz odczuwać pokusę, aby ją zapieczętować, aby ludzie nie mogli jej używać w sposób, którego nie przewidziałeś (i nie jesteś przygotowany). Z drugiej strony, kiedy używasz cudzego kodu, czasem z publicznego API nie ma widocznego powodu, aby klasa była ostateczna, i możesz pomyśleć „Cholera, to tylko kosztowało mnie 30 minut w poszukiwaniu obejścia”.
Myślę, że niektóre elementy rozwiązania to:
Warto również wspomnieć o następującym „filozoficznym” pytaniu: czy klasa jest
sealed
/ jestfinal
częścią kontraktu dla klasy, czy szczegół implementacyjny? Teraz nie chcę tam iść, ale odpowiedź na to pytanie powinna również wpłynąć na twoją decyzję, czy zapieczętować klasę, czy nie.źródło
Klasa nie powinna być ostateczna / zapieczętowana ani abstrakcyjna, jeżeli:
Weźmy na przykład
ObservableCollection<T>
klasę w języku C # . Musi jedynie dodać wywoływanie zdarzeń do normalnych operacji aCollection<T>
, dlatego podklasujeCollection<T>
.Collection<T>
sama w sobie jest opłacalną klasą i tak też jestObservableCollection<T>
.źródło
CollectionBase
(streszczenie),Collection : CollectionBase
(zapieczętowane),ObservableCollection : CollectionBase
(zapieczętowane). Jeśli przyjrzysz się uważnie kolekcji <T> , zobaczysz, że jest to klasa abstrakcyjna w połowie oceniona.Collection<T>
sposób „na wpół oceniana klasa abstrakcyjna”?Collection<T>
odsłania wiele elementów wewnętrznych dzięki chronionym metodom i właściwościom, i wyraźnie ma być klasą podstawową dla specjalistycznej kolekcji. Ale nie jest jasne, gdzie należy umieścić kod, ponieważ nie ma żadnych abstrakcyjnych metod do implementacji.ObservableCollection
dziedziczy poCollection
i nie jest zapieczętowany, więc możesz ponownie odziedziczyć po nim. I możesz uzyskać dostęp doItems
chronionej nieruchomości, co pozwala dodawać elementy do kolekcji bez podnoszenia zdarzeń ... Fajnie.Problem z końcowymi / zapieczętowanymi klasami polega na tym, że próbują rozwiązać problem, który jeszcze się nie zdarzył. Jest to przydatne tylko wtedy, gdy problem istnieje, ale jest frustrujący, ponieważ firma zewnętrzna nałożyła ograniczenia. Rzadko klasa uszczelnienia rozwiązuje bieżący problem, co utrudnia argumentowanie jego przydatności.
Są przypadki, w których klasa powinna być zapieczętowana. Jako przykład; Klasa zarządza przydzielonymi zasobami / pamięcią w taki sposób, że nie jest w stanie przewidzieć, w jaki sposób przyszłe zmiany mogą zmienić to zarządzanie.
Przez lata odkryłem, że enkapsulacja, wywołania zwrotne i zdarzenia są o wiele bardziej elastyczne / przydatne niż abstrakcyjne klasy. Widzę za dużo kodu z dużą hierarchią klas, w których enkapsulacja i zdarzenia uprościłyby życie programistom.
źródło
„Uszczelnianie” lub „finalizowanie” w systemach obiektowych pozwala na pewne optymalizacje, ponieważ znany jest pełny wykres wysyłki.
Oznacza to, że utrudniamy zmianę systemu na coś innego, co jest kompromisem w zakresie wydajności. (To jest istota większości optymalizacji.)
Pod wszystkimi innymi względami jest to strata. Systemy powinny być domyślnie otwarte i rozszerzalne. Powinno być łatwe dodawanie nowych metod do wszystkich klas i dowolne rozszerzanie.
Nie zyskujemy dziś żadnej nowej funkcji, podejmując kroki zapobiegające przyszłemu rozszerzeniu.
Jeśli więc robimy to w celu samej prewencji, to staramy się kontrolować życie przyszłych opiekunów. Chodzi o ego. „Nawet, gdy już tu nie będę pracować, ten kod zostanie zachowany, do cholery!”
źródło
Wydaje się, że przychodzą na myśl klasy testowe. Są to klasy wywoływane w sposób zautomatyzowany lub „do woli” na podstawie tego, co programista / tester próbuje osiągnąć. Nie jestem pewien, czy kiedykolwiek widziałem lub nie słyszałem o skończonej klasie testów prywatnych.
źródło
Czas, w którym musisz rozważyć zajęcia, które mają zostać przedłużone, to czas, kiedy planujesz prawdziwe plany na przyszłość. Pozwól, że dam ci prawdziwy przykład z mojej pracy.
Sporo czasu spędzam na pisaniu narzędzi interfejsu między naszym głównym produktem a systemami zewnętrznymi. Gdy dokonujemy nowej sprzedaży, jednym dużym komponentem jest zestaw eksporterów, które są zaprojektowane do uruchamiania w regularnych odstępach czasu, które generują pliki danych szczegółowo opisujące zdarzenia, które miały miejsce tego dnia. Te pliki danych są następnie zużywane przez system klienta.
To doskonała okazja do przedłużenia zajęć.
Mam klasę eksportu, która jest klasą podstawową każdego eksportera. Wie, jak połączyć się z bazą danych, dowiedzieć się, gdzie musiała ostatnio działać, i utworzyć archiwa generowanych plików danych. Zapewnia również zarządzanie plikami właściwości, rejestrowanie i kilka prostych procedur obsługi wyjątków.
Ponadto mam innego eksportera do pracy z każdym rodzajem danych, być może istnieje aktywność użytkownika, dane transakcyjne, dane zarządzania gotówką itp.
Na szczycie tego stosu umieszczam warstwę specyficzną dla klienta, która implementuje strukturę pliku danych, której potrzebuje klient.
W ten sposób podstawowy eksporter bardzo rzadko się zmienia. Eksporterzy podstawowych typów danych czasami się zmieniają, ale rzadko, i zwykle tylko w celu obsługi zmian schematu bazy danych, które i tak powinny zostać rozpowszechnione wśród wszystkich klientów. Jedyną pracą, jaką muszę wykonać dla każdego klienta, jest ta część kodu, która jest specyficzna dla tego klienta. Idealny świat!
Struktura wygląda więc tak:
Chodzi przede wszystkim o to, że dzięki takiej architekturze kodu mogę wykorzystać dziedziczenie przede wszystkim do ponownego użycia kodu.
Muszę powiedzieć, że nie mogę wymyślić żadnego powodu, by przejść trzy warstwy.
Wielokrotnie używałem dwóch warstw, na przykład, aby mieć wspólną
Table
klasę, która implementuje zapytania do tabel bazy danych, podczas gdy podklasyTable
implementują określone szczegóły każdej tabeli, używającenum
do zdefiniowania pól. Pozwalającenum
implementować interfejs zdefiniowany wTable
klasie sprawia, że wszystkie rodzaje sensu.źródło
Uważam, że klasy abstrakcyjne są użyteczne, ale nie zawsze potrzebne, a klasy zapieczętowane stają się problemem podczas stosowania testów jednostkowych. Nie możesz kpić ani ukrywać zapieczętowanej klasy, chyba że użyjesz czegoś takiego jak Teleriks justmock.
źródło
Zgadzam się z twoim poglądem. Myślę, że w Javie klasy powinny być domyślnie „ostateczne”. Jeśli nie uczynisz tego ostatecznym, przygotuj go i udokumentuj do rozszerzenia.
Głównym tego powodem jest zapewnienie, że wszelkie wystąpienia twoich klas będą zgodne z interfejsem, który pierwotnie zaprojektowałeś i udokumentowałeś. W przeciwnym razie programista korzystający z twoich klas może potencjalnie stworzyć kruchy i niespójny kod, a następnie przekazać go innym programistom / projektom, czyniąc obiekty twoich klas niewiarygodnymi.
Z bardziej praktycznego punktu widzenia jest to rzeczywiście wada, ponieważ klienci interfejsu twojej biblioteki nie będą mogli dokonywać żadnych poprawek i używać twoich klas w bardziej elastyczny sposób, o jakim początkowo myślałeś.
Osobiście, biorąc pod uwagę cały zły kod jakości, który istnieje (a ponieważ dyskutujemy o tym na bardziej praktycznym poziomie, argumentowałbym, że programowanie w Javie jest bardziej podatne na to). Myślę, że ta sztywność jest niewielką ceną do zapłaty w naszym dążeniu do łatwiejszego utrzymywać kod.
źródło