Rozmawiałem ze współpracownikiem i skończyły się sprzeczne intuicje na temat celu podklasy. Moją intuicją jest to, że jeśli podstawową funkcją podklasy jest wyrażenie ograniczonego zakresu możliwych wartości jej elementu nadrzędnego, prawdopodobnie nie powinna to być podklasa. Argumentował za odwrotną intuicją: ta podklasa reprezentuje bardziej „specyficzny” obiekt, a zatem relacja podklasy jest bardziej odpowiednia.
Mówiąc bardziej konkretnie, uważam, że jeśli mam podklasę, która rozszerza klasę nadrzędną, ale jedynym kodem, który zastępuje podklasę, jest konstruktor (tak, wiem, że konstruktory na ogół nie „nadpisują”, trzymaj się mnie), to tak naprawdę potrzebna była metoda pomocnicza.
Rozważmy na przykład nieco prawdziwą klasę:
public class DataHelperBuilder
{
public string DatabaseEngine { get; set; }
public string ConnectionString { get; set; }
public DataHelperBuilder(string databaseEngine, string connectionString)
{
DatabaseEngine = databaseEngine;
ConnectionString = connectionString;
}
// Other optional "DataHelper" configuration settings omitted
public DataHelper CreateDataHelper()
{
Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
dh.SetConnectionString(ConnectionString);
// Omitted some code that applies decorators to the returned object
// based on omitted configuration settings
return dh;
}
}
Twierdzi, że posiadanie takiej podklasy byłoby całkowicie właściwe:
public class SystemDataHelperBuilder
{
public SystemDataHelperBuilder()
: base(Configuration.GetSystemDatabaseEngine(),
Configuration.GetSystemConnectionString())
{
}
}
Tak więc pytanie:
- Która z tych intuicji jest poprawna wśród ludzi, którzy mówią o wzorach projektowych? Czy podklasa, jak opisano powyżej, jest anty-wzorem?
- Jeśli jest to anty-wzór, jak się nazywa?
Przepraszam, jeśli okaże się, że jest to odpowiedź łatwa do przeszukiwania; moje wyszukiwania w google zwróciły głównie informacje na temat anty-wzorca teleskopowego konstruktora, a nie tak naprawdę tego, czego szukałem.
źródło
Odpowiedzi:
Jeśli wszystko, co chcesz zrobić, to stworzyć klasę X z pewnymi argumentami, podklasowanie jest dziwnym sposobem wyrażenia tej intencji, ponieważ nie używasz żadnej funkcji, którą dają ci klasy i dziedziczenie. To nie jest tak naprawdę anty-wzór, jest po prostu dziwny i trochę bezcelowy (chyba że masz inne powody). Bardziej naturalnym sposobem wyrażenia tego zamiaru byłaby Metoda Fabryczna , która w tym przypadku jest fantazyjną nazwą twojej „metody pomocniczej”.
Jeśli chodzi o ogólną intuicję, zarówno „bardziej szczegółowy”, jak i „ograniczony zasięg” są potencjalnie szkodliwymi sposobami myślenia o podklasach, ponieważ obie sugerują, że uczynienie z Kwadratu podklasy Prostokąta jest dobrym pomysłem. Bez polegania na czymś formalnym, takim jak LSP, powiedziałbym, że lepszą intuicją jest to, że podklasa zapewnia implementację interfejsu podstawowego lub rozszerza interfejs, aby dodać nowe funkcje.
źródło
SystemDataHelperBuilder
konkretnie.Twój współpracownik ma rację (zakładając standardowe systemy typów).
Pomyśl o tym, klasy reprezentują możliwe wartości prawne. Jeśli klasa
A
ma jedno bajtowe poleF
, możesz mieć skłonność do myślenia, żeA
ma 256 legalnych wartości, ale ta intuicja jest nieprawidłowa.A
ogranicza „każdą permutację wartości kiedykolwiek” do „pole musi mieć wartośćF
0–255”.Jeśli przedłużyć
A
zB
którym ma innego pola bajtFF
, który jest dodanie ograniczeń . Zamiast „wartość gdzieF
bajt” masz „wartość gdzieF
bajt &&FF
także bajt”. Ponieważ zachowujesz stare reguły, wszystko, co działało dla typu bazowego, nadal działa dla twojego podtypu. Ale ponieważ dodajesz reguły, dalej specjalizujesz się od „może być wszystko”.Lub myśleć o nim inaczej, zakładamy, że
A
miał kilka podtypów:B
,C
, iD
. Zmienna typuA
może być dowolnym z tych podtypów, ale zmienna typuB
jest bardziej szczegółowa. To (w większości systemów typu) nie może być aC
lub aD
.Enh? Posiadanie wyspecjalizowanych obiektów jest przydatne i nie jest wyraźnie szkodliwe dla kodu. Osiągnięcie tego wyniku za pomocą podtypu jest być może nieco wątpliwe, ale zależy od tego, jakie narzędzia masz do dyspozycji. Nawet przy lepszych narzędziach ta implementacja jest prosta, prosta i niezawodna.
Nie uważałbym tego za anty-wzór, ponieważ nie jest to wyraźnie złe i złe w żadnej sytuacji.
źródło
byte and byte
zdecydowanie ma więcej wartości niż tylko typbyte
; 256 razy więcej. Poza tym nie sądzę, aby można było pogodzić reguły z liczbą elementów (lub licznością, jeśli chcesz być precyzyjnym). Rozkłada się, gdy rozważasz zestawy nieskończone. Jest tyle samo liczb parzystych, ile jest liczb całkowitych, ale wiedza o tym, że liczba jest parzysta, nadal stanowi ograniczenie / daje mi więcej informacji niż tylko wiedza, że to liczba całkowita! To samo można powiedzieć o liczbach naturalnych.n -> 2n
). Nie zawsze jest to możliwe; nie można odwzorować liczb całkowitych na liczby rzeczywiste, więc zestaw liczb rzeczywistych jest „większy” niż liczby całkowite. Ale możesz odwzorować (rzeczywisty) przedział [0, 1] na zbiór wszystkich liczb rzeczywistych, dzięki czemu będą miały ten sam „rozmiar”. Podzbiory są zdefiniowane jako „każdy elementA
jest również elementemB
” właśnie dlatego, że gdy twoje zbiory stają się nieskończone, może się zdarzyć, że mają one tę samą liczność, ale mają różne elementy.Nie, to nie jest anty-wzór. Mogę wymyślić kilka praktycznych przypadków użycia w tym celu:
MySQLDao
iSqliteDao
w systemie, ale z jakiegoś powodu chcesz się upewnić, zbiór zawiera tylko dane z jednego źródła, jeśli używasz podklasy jak opisać można mieć kompilator zweryfikować tę szczególną prawidłowość. Z drugiej strony, jeśli źródło danych jest przechowywane jako pole, byłoby to sprawdzanie w czasie wykonywania.AlgorithmConfiguration
+ProductClass
aAlgorithmInstance
. Innymi słowy, jeśli skonfigurujeszFooAlgorithm
dlaProductClass
w pliku właściwości, możesz uzyskać tylko jedenFooAlgorithm
dla tej klasy produktu. Jedynym sposobem na uzyskanie dwóchFooAlgorithm
dla danejProductClass
jest utworzenie podklasyFooBarAlgorithm
. Jest to w porządku, ponieważ sprawia, że plik właściwości jest łatwy w użyciu (nie ma potrzeby składni dla wielu różnych konfiguracji w jednej sekcji pliku właściwości), i bardzo rzadko występuje więcej niż jedna instancja dla danej klasy produktu.Konkluzja: Nie ma w tym nic złego. Anti-wzór określa się:
Nie ma tutaj ryzyka, że będzie on przynosił efekt przeciwny do zamierzonego, więc nie jest to anty-wzór.
źródło
Jeśli coś jest wzorem lub anty-wzorem, zależy w znacznym stopniu od języka i środowiska, w którym piszesz. Na przykład,
for
pętle są wzorcem w asemblerze, C i podobnych językach oraz anty-wzorcem w lisp.Pozwól, że opowiem ci historię kodu napisanego dawno temu ...
Został napisany w języku LPC i zaimplementował strukturę rzucania czarów w grze. Miałeś nadklasę zaklęcia, która miała podklasę zaklęć bojowych, która poradziła sobie z pewnymi czekami, a następnie podklasę zaklęć zadających bezpośrednie obrażenia jednokierunkowe, które następnie zostały zaklasyfikowane do poszczególnych zaklęć - pocisku magicznego, błyskawicy i tym podobnych.
Charakter działania LPC polegał na tym, że miałeś główny obiekt, którym był Singleton. zanim przejdziesz do „eww Singleton” - było i wcale nie było „eww”. Nie wykonano kopii zaklęć, ale przywołano (skutecznie) metody statyczne w zaklęciach. Kod magicznego pocisku wyglądał następująco:
Widzisz to? Jest to klasa tylko dla konstruktorów. Ustawił pewne wartości, które były w nadrzędnej klasie abstrakcyjnej (nie, nie powinien używać setterów - jest niezmienny) i tyle. Działa dobrze . Kodowanie było łatwe, łatwe do rozszerzenia i łatwe do zrozumienia.
Ten rodzaj wzorca przekłada się na inne sytuacje, w których masz podklasę, która rozszerza klasę abstrakcyjną i ustawia kilka własnych wartości, ale cała funkcjonalność znajduje się w klasie abstrakcyjnej, którą dziedziczy. Rzuć okiem na StringBuilder w Javie, aby zobaczyć przykład tego - nie do końca konstruktora, ale jestem zmuszony znaleźć dużo logiki w samych metodach (wszystko jest w AbstractStringBuilder).
Nie jest to zła rzecz, a na pewno nie jest to anty-wzór (w niektórych językach może to być sam wzór).
źródło
Najczęstszym zastosowaniem takich podklas, o których mogę myśleć, są hierarchie wyjątków, chociaż jest to zdegenerowany przypadek, w którym zwykle nie definiowalibyśmy niczego w klasie, o ile język pozwala nam dziedziczyć konstruktory. Używamy dziedziczenia, aby wyrazić, że
ReadError
jest to szczególny przypadekIOError
i tak dalej, aleReadError
nie musimy zastępować żadnych metodIOError
.Robimy to, ponieważ wychwytuje się wyjątki, sprawdzając ich typ. Dlatego musimy zakodować specjalizację w typie, aby ktoś mógł łapać tylko
ReadError
bez łapania wszystkiegoIOError
, jeśli tego chce.Zwykle sprawdzanie rodzajów rzeczy jest strasznie złą formą i staramy się tego unikać. Jeśli uda nam się tego uniknąć, nie ma potrzeby wyrażania specjalizacji w systemie typów.
Może się jednak okazać przydatny, na przykład, jeśli nazwa typu pojawia się w zapisanym ciągu znaków obiektu: jest to język i możliwe specyficzne dla frameworka. Można w zasadzie wymienić nazwę typu w każdym takim systemie z wywołaniem metody klasy, w tym przypadku mamy byłoby nadpisać więcej niż tylko konstruktora
SystemDataHelperBuilder
, my również zastąpićloggingname()
. Więc jeśli w twoim systemie jest takie logowanie, możesz sobie wyobrazić, że chociaż twoja klasa dosłownie zastępuje konstruktor, „moralnie” to także przesłania nazwę klasy, a więc dla celów praktycznych nie jest to podklasa tylko konstruktora. Ma pożądane inne zachowanie, to „Powiedziałbym więc, że jego kod może być dobrym pomysłem, jeśli jest gdzie indziej jakiś kod, który w jakiś sposób sprawdza instancje
SystemDataHelperBuilder
, albo jawnie z gałęzią (np. Gałęzie wychwytujące wyjątki na podstawie typu), albo może dlatego, że istnieje jakiś kod ogólny, który się skończy używanie nazwySystemDataHelperBuilder
w użyteczny sposób (nawet jeśli jest to tylko logowanie).Jednak używanie typów do specjalnych przypadków prowadzi do pewnych nieporozumień, ponieważ jeśli
ImmutableRectangle(1,1)
iImmutableSquare(1)
zachowują się inaczej w jakikolwiek sposób, w końcu ktoś będzie się zastanawiał, dlaczego i być może nie chciałby. W przeciwieństwie do hierarchii wyjątków, sprawdzasz, czy prostokąt jest kwadratem, sprawdzając, czyheight == width
nieinstanceof
. W twoim przykładzie istnieje różnica między aSystemDataHelperBuilder
aDataHelperBuilder
utworzonym z dokładnie tymi samymi argumentami i może to powodować problemy. Dlatego funkcja zwracania obiektu utworzonego za pomocą „właściwych” argumentów wydaje mi się zwykle właściwą rzeczą: podklasa daje dwie różne reprezentacje „tej samej” rzeczy i pomaga tylko wtedy, gdy robisz coś normalnie starałbym się tego nie robić.Zauważ, że nie mówię o wzorcach projektowych i anty-wzorach, ponieważ nie wierzę, że można zapewnić taksonomię wszystkich dobrych i złych pomysłów, o których programista kiedykolwiek myślał. Tak więc, nie każdy pomysł jest wzorem lub anty-wzorcem, staje się tak, że gdy ktoś rozpozna, że pojawia się wielokrotnie w różnych kontekstach i nazywa go ;-) Nie znam żadnej konkretnej nazwy dla tego rodzaju podklasy.
źródło
ImmutableSquareMatrix:ImmutableMatrix
, pod warunkiem, że istnieje skuteczny sposób, aby poprosić oImmutableMatrix
renderowanie jego zawartości,ImmutableSquareMatrix
jeśli to możliwe. Nawet jeśli wImmutableSquareMatrix
zasadzie był toImmutableMatrix
konstruktor, który wymagał dopasowania wysokości i szerokości, i któregoAsSquareMatrix
metoda po prostu zwróciłaby się (zamiast np. Konstruowania tego,ImmutableSquareMatrix
który otacza tę samą tablicę bazową), taki projekt umożliwiłby walidację parametrów czasu kompilacji dla metod wymagających kwadratu matryceSystemDataHelperBuilder
, a nie tylko jakaśDataHelperBuilder
, to ma sens zdefiniowanie typu dla tego (i interfejsu, zakładając, że obsługuje się DI w ten sposób) . W twoim przykładzie istnieją pewne operacje, które mogłaby mieć macierz kwadratowa, których nie zrobiłby kwadrat, takie jak wyznacznik, ale masz rację, że nawet jeśli system nie potrzebuje tych, które nadal chcesz sprawdzić według typu .height == width
nieinstanceof
”. Możesz preferować coś, co możesz twierdzić statycznie.foo=new ImmutableMatrix(someReadableMatrix)
jest wyraźniejszy niżsomeReadableMatrix.AsImmutable()
lub nawetImmutableMatrix.Create(someReadableMatrix)
, ale te ostatnie formy oferują użyteczne możliwości semantyczne, których wcześniej brak; jeśli konstruktor może stwierdzić, że macierz jest kwadratowa, możliwość odzwierciedlenia jej typu może być bardziej przejrzysta niż wymaganie kodu w dół, który wymaga kwadratowej macierzy do utworzenia nowej instancji obiektu.new
operatora. Klasa to tylko wywoływalna funkcja, która po wywołaniu zwraca (domyślnie) instancję samego siebie. Jeśli więc chcesz zachować spójność interfejsu, możesz napisać funkcję fabryczną, której nazwa zaczyna się od dużej litery, dlatego wygląda to jak wywołanie konstruktora. Nie dlatego, że robię to rutynowo z funkcjami fabrycznymi, ale jest tam, kiedy jest to potrzebne.Najściślejsza zorientowana obiektowo definicja relacji podklasy jest znana jako «is-a». Korzystając z tradycyjnego przykładu kwadratu i prostokąta, kwadrat «jest» prostokątem.
Dzięki temu powinieneś używać podklasy w każdej sytuacji, w której uważasz, że „is-a” jest znaczącym związkiem dla twojego kodu.
W konkretnym przypadku podklasy bez konstruktora powinieneś przeanalizować ją z perspektywy „jest”. Oczywiste jest, że SystemDataHelperBuilder «is-a» DataHelperBuilder, więc pierwszy przebieg sugeruje, że jest to poprawne użycie.
Pytanie, na które powinieneś odpowiedzieć, brzmi: czy jakikolwiek inny kod wykorzystuje tę relację „jest”. Czy jakiś inny kod próbuje odróżnić SystemDataHelperBuilders od ogólnej populacji DataHelperBuilders? Jeśli tak, związek jest całkiem rozsądny i należy zachować podklasę.
Jeśli jednak żaden inny kod tego nie robi, to masz relację, która może być relacją typu „jest-a” lub może zostać zaimplementowana w fabryce. W takim przypadku możesz pójść w obie strony, ale zaleciłbym raczej fabrykę niż relację typu „jest-a”. Podczas analizy kodu obiektowego napisanego przez inną osobę, hierarchia klas jest głównym źródłem informacji. Zapewnia relacje «is-a», których będą potrzebować, aby zrozumieć kod. Odpowiednio, umieszczenie tego zachowania w podklasie promuje zachowanie od „czegoś, co ktoś znajdzie, jeśli zagłębi się w metody nadklasy”, do „czegoś, przez co wszyscy przejrzą, zanim zaczną nurkować w kodzie”. Cytując Guido van Rossuma, „kod jest czytany częściej niż jest napisany” więc zmniejszenie czytelności kodu dla przyszłych programistów jest sztywną ceną do zapłacenia. Wolałbym nie płacić, gdybym zamiast tego mógł skorzystać z fabryki.
źródło
Podtyp nigdy nie jest „zły”, o ile zawsze możesz zastąpić instancję jego nadtypu instancją podtypu i mieć pewność, że wszystko nadal działa poprawnie. Sprawdza się to tak długo, jak podklasa nigdy nie próbuje osłabić żadnej z gwarancji, jakie daje nadtyp. Może dawać silniejsze (bardziej szczegółowe) gwarancje, więc w tym sensie intuicja współpracownika jest poprawna.
Biorąc to pod uwagę, prawdopodobnie twoja podklasa nie jest zła (ponieważ nie wiem dokładnie, co oni robią, nie mogę powiedzieć tego na pewno). Musisz uważać na delikatny problem z klasą podstawową, a także musisz zastanów się, czy
interface
przyniosłoby ci to korzyść, ale dotyczy to wszystkich zastosowań dziedziczenia.źródło
Myślę, że kluczem do odpowiedzi na to pytanie jest spojrzenie na ten konkretny scenariusz użytkowania.
Dziedziczenie służy do egzekwowania opcji konfiguracji bazy danych, takich jak parametry połączenia.
Jest to anty-wzorzec, ponieważ klasa narusza zasadę pojedynczej odpowiedzialności --- konfiguruje się z określonym źródłem tej konfiguracji, a nie z „robieniem jednej rzeczy i robieniem tego dobrze”. Konfiguracja klasy nie powinna być wprowadzana do samej klasy. W przypadku ustawiania parametrów połączenia z bazą danych większość razy widziałem, że dzieje się to na poziomie aplikacji podczas jakiegoś zdarzenia, które ma miejsce podczas uruchamiania aplikacji.
źródło
Używam tylko podklas konstruktora dość regularnie, aby wyrazić różne koncepcje.
Rozważmy na przykład następującą klasę:
Następnie możemy utworzyć podklasę:
Następnie możesz użyć CapitalizeAndPrintOutputter w dowolnym miejscu w kodzie i dokładnie wiesz, jaki to typ programu wyjściowego. Co więcej, ten wzór w rzeczywistości bardzo ułatwia użycie kontenera DI do stworzenia tej klasy. Jeśli kontener DI wie o PrintOutputMethod i Capitalizer, może nawet automatycznie utworzyć dla Ciebie CapitalizeAndPrintOutputter.
Korzystanie z tego wzorca jest naprawdę dość elastyczne i przydatne. Tak, można użyć metod fabrycznych zrobić to samo, ale to (przynajmniej w C #) można stracić niektóre z mocą systemu typu i na pewno przy użyciu kontenerów DI staje się coraz bardziej uciążliwe.
źródło
Pragnę najpierw zauważyć, że podczas pisania kodu podklasa nie jest tak naprawdę bardziej szczegółowa niż element nadrzędny, ponieważ oba zainicjowane pola można ustawiać według kodu klienta.
Wystąpienie podklasy można rozróżnić tylko za pomocą downcastu.
Teraz, jeśli masz projekt, w którym właściwości
DatabaseEngine
iConnectionString
zostały ponownie zaimplementowane przez podklasę, któraConfiguration
miała do tego dostęp, masz ograniczenie, które bardziej sensowne jest posiadanie podklasy.Powiedziawszy to, „anty-wzór” jest zbyt silny jak na mój gust; nie ma tu nic naprawdę złego.
źródło
W podanym przykładzie twój partner ma rację. Dwóch konstruktorów pomocników danych to strategie. Jedno jest bardziej szczegółowe niż drugie, ale oba są wymienne po zainicjowaniu.
Zasadniczo, gdyby klient konstruktora danych miał otrzymać systemowy konstruktor danych, nic by się nie zepsuło. Inną opcją byłoby, gdyby systemdatahelperbuilder był statycznym wystąpieniem klasy datahelperbuilder.
źródło