Podklasy przeznaczone wyłącznie dla konstruktorów: czy jest to anty-wzór?

37

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:

  1. 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?
  2. 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.

HardlyKnowEm
źródło
2
Uważam, że słowo „Pomocnik” w nazwie jest antypatternem. To tak, jakby czytać esej ze stałymi odniesieniami do rzeczy i odpowiedzi. Powiedz co to jest . Czy to jest fabryka? Konstruktor? Dla mnie pachnie to leniwym procesem myślowym i zachęca do naruszenia zasady pojedynczej odpowiedzialności, ponieważ kieruje nim właściwa nazwa semantyczna. Zgadzam się z innymi wyborcami i powinieneś zaakceptować odpowiedź @ lxrec. Tworzenie podklasy dla metody fabrycznej jest całkowicie bezcelowe, chyba że nie można ufać użytkownikom, że przekażą poprawne argumenty określone w dokumentacji. W takim przypadku pozyskaj nowych użytkowników.
Aaron Hall
Absolutnie zgadzam się z odpowiedzią @ Ixrec. W końcu jego odpowiedź mówi, że cały czas miałem rację, a mój współpracownik się mylił. ;-) Ale tak na poważnie, właśnie dlatego czułem się dziwnie, akceptując to; Myślałem, że mogę zostać oskarżony o stronniczość. Ale jestem nowy dla programistów StackExchange; Byłbym skłonny to zaakceptować, gdybym był pewien, że nie byłoby to naruszeniem etykiety.
HardlyKnowEm
1
Nie, oczekuje się, że zaakceptujesz odpowiedź, którą uważasz za najcenniejszą. Społeczność, która jak dotąd uważa za najcenniejszą, to zdecydowanie Ixreca o ponad 3 do 1. W związku z tym oczekują, że ją zaakceptujesz. W tej społeczności bardzo mało jest frustracji u pytającego przyjmującego złą odpowiedź, ale jest duża frustracja u pytających, którzy nigdy nie przyjmują odpowiedzi. Zauważ, że nikt nie narzeka na przyjętą tutaj odpowiedź, zamiast tego narzekają na głosowanie, które wydaje się słuszne: stackoverflow.com/q/2052390/541136
Aaron Hall
Bardzo dobrze, zaakceptuję to. Dziękujemy za poświęcenie czasu na edukację na temat standardów społeczności tutaj.
HardlyKnowEm,

Odpowiedzi:

54

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.

Ixrec
źródło
2
Metody fabryczne nie pozwalają jednak wymuszać pewnych zachowań (rządzonych argumentami) w bazie kodu. A czasem przydatne jest wyraźne odnotowanie, że ta klasa / metoda / pole zajmuje SystemDataHelperBuilderkonkretnie.
Telastyn
3
Ach, myślę, że argument kwadrat / prostokąt był dokładnie tym, czego szukałem. Dzięki!
HardlyKnowEm
7
Oczywiście posiadanie kwadratu jako podklasy prostokąta może być w porządku, jeśli są niezmienne. Naprawdę musisz spojrzeć na LSP.
David Conrad
10
Problem związany z kwadratem / prostokątem polega na tym, że kształty geometryczne są niezmienne. Zawsze możesz użyć kwadratu, gdziekolwiek prostokąt ma sens, ale zmiana rozmiaru nie jest czymś, co robią kwadraty lub prostokąty.
Doval
3
@nha LSP = Liskov Substitution Principle
Ixrec
16

Która z tych intuicji jest poprawna?

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 Ama jedno bajtowe pole F, możesz mieć skłonność do myślenia, że Ama 256 legalnych wartości, ale ta intuicja jest nieprawidłowa. Aogranicza „każdą permutację wartości kiedykolwiek” do „pole musi mieć wartość F0–255”.

Jeśli przedłużyć Az Bktórym ma innego pola bajt FF, który jest dodanie ograniczeń . Zamiast „wartość gdzie Fbajt” masz „wartość gdzie Fbajt && FFtakż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 Amiał kilka podtypów: B, C, i D. Zmienna typu Amoże być dowolnym z tych podtypów, ale zmienna typu Bjest bardziej szczegółowa. To (w większości systemów typu) nie może być a Club a D.

Czy to jest anty-wzór?

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.

Telastyn
źródło
5
„Więcej reguł oznacza mniej możliwych wartości, oznacza bardziej szczegółowe”. Naprawdę tego nie śledzę. Typ byte and bytezdecydowanie ma więcej wartości niż tylko typ byte; 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.
Doval
6
@Telastyn Zaczynamy od tematu, ale można udowodnić, że liczność (luźno „rozmiar”) zbioru parzystych liczb całkowitych jest taka sama jak zbioru wszystkich liczb całkowitych, a nawet wszystkich liczb wymiernych. To naprawdę fajne rzeczy. Zobacz math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm
2
Teoria zbiorów jest dość nieintuicyjna. Możesz wstawić liczby całkowite i równe do korespondencji jeden do jednego (np. Używając funkcji 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 element Ajest również elementem B” 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.
Doval
1
Bardziej związany z omawianym tematem, podany przez ciebie przykład jest ograniczeniem, które pod względem programowania obiektowego faktycznie rozszerza - funkcjonalność - pod względem dodatkowego pola. Mówię o podklasach, które nie rozszerzają funkcjonalności - jak na przykład problem elipsy koła.
HardlyKnowEm
1
@Doval: Istnieje więcej niż jedno możliwe znaczenie „mniej” - tj. Więcej niż jedna relacja porządkowa na zbiorach. Relacja „jest podzbiorem” daje dobrze zdefiniowane (częściowe) uporządkowanie zbiorów. Ponadto, „więcej reguł” może oznaczać, że „wszystkie elementy zbioru spełniają więcej (tj. Zbiór) zdań (z określonego zbioru)”. Dzięki tym definicjom „więcej reguł” rzeczywiście oznacza „mniej wartości”. Jest to, z grubsza mówiąc, podejście przyjęte przez szereg modeli semantycznych programów komputerowych, np. Domen Scott.
psmears
12

Nie, to nie jest anty-wzór. Mogę wymyślić kilka praktycznych przypadków użycia w tym celu:

  1. Jeśli chcesz użyć sprawdzania czasu kompilacji, aby upewnić się, że zbiory obiektów są zgodne tylko z jedną określoną podklasą. Na przykład, jeśli masz MySQLDaoi SqliteDaow 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.
  2. Moja obecna aplikacja ma relację jeden do jednego między AlgorithmConfiguration+ ProductClassa AlgorithmInstance. Innymi słowy, jeśli skonfigurujesz FooAlgorithmdla ProductClassw pliku właściwości, możesz uzyskać tylko jeden FooAlgorithmdla tej klasy produktu. Jedynym sposobem na uzyskanie dwóch FooAlgorithmdla danej ProductClassjest utworzenie podklasy FooBarAlgorithm. 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.
  3. Odpowiedź Telastyna daje kolejny dobry przykład.

Konkluzja: Nie ma w tym nic złego. Anti-wzór określa się:

Anty-wzór (lub anty-wzór) jest częstą reakcją na powtarzający się problem, który zwykle jest nieskuteczny i grozi odwrotnym skutkiem .

Nie ma tutaj ryzyka, że ​​będzie on przynosił efekt przeciwny do zamierzonego, więc nie jest to anty-wzór.

durron597
źródło
2
Tak, myślę, że gdybym musiał powtórzyć pytanie, powiedziałbym „zapach kodu”. Nie jest wystarczająco zły, aby uzasadnić ostrzeżenie młodego pokolenia młodych programistów, aby go unikał, ale jest to coś, co jeśli go zobaczysz, może wskazywać na problem z nadrzędnym projektem, ale ostatecznie może być również poprawny.
HardlyKnowEm
Punkt 1 jest odpowiedni do celu. Naprawdę nie rozumiałem punktu 2, ale jestem pewien, że jest tak samo dobry ... :)
Phil
9

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, forpę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:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

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).

Społeczność
źródło
2

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 ReadErrorjest to szczególny przypadek IOErrori tak dalej, ale ReadErrornie musimy zastępować żadnych metod IOError.

Robimy to, ponieważ wychwytuje się wyjątki, sprawdzając ich typ. Dlatego musimy zakodować specjalizację w typie, aby ktoś mógł łapać tylko ReadErrorbez łapania wszystkiego IOError, 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 nazwy SystemDataHelperBuilderw 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)i ImmutableSquare(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, czy height == widthnie instanceof. W twoim przykładzie istnieje różnica między a SystemDataHelperBuildera DataHelperBuilderutworzonym 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.

Steve Jessop
źródło
Widziałem zastosowania dla ImmutableSquareMatrix:ImmutableMatrix, pod warunkiem, że istnieje skuteczny sposób, aby poprosić o ImmutableMatrixrenderowanie jego zawartości, ImmutableSquareMatrixjeśli to możliwe. Nawet jeśli w ImmutableSquareMatrixzasadzie był to ImmutableMatrixkonstruktor, który wymagał dopasowania wysokości i szerokości, i którego AsSquareMatrixmetoda po prostu zwróciłaby się (zamiast np. Konstruowania tego, ImmutableSquareMatrixktóry otacza tę samą tablicę bazową), taki projekt umożliwiłby walidację parametrów czasu kompilacji dla metod wymagających kwadratu matryce
supercat
@supercat: dobra uwaga, aw przykładzie pytającego, jeśli istnieją funkcje, które należy przekazać a SystemDataHelperBuilder, 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 .
Steve Jessop
... więc przypuszczam, że to nie do końca prawda, co powiedziałem, że „sprawdzasz, czy prostokąt jest kwadratem, sprawdzając, czy height == widthnie instanceof”. Możesz preferować coś, co możesz twierdzić statycznie.
Steve Jessop
Chciałbym, żeby istniał mechanizm, który pozwalałby na wywołanie statycznych metod fabrycznych, takich jak składnia konstruktora. Rodzaj wyrażenia foo=new ImmutableMatrix(someReadableMatrix)jest wyraźniejszy niż someReadableMatrix.AsImmutable()lub nawet ImmutableMatrix.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.
supercat
@ superupat: Jedną małą rzeczą w Pythonie, którą lubię, jest brak newoperatora. 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.
Steve Jessop
2

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.

Cort Ammon
źródło
1

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 interfaceprzyniosłoby ci to korzyść, ale dotyczy to wszystkich zastosowań dziedziczenia.

Doval
źródło
1

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.

Greg Burghardt
źródło
1

Używam tylko podklas konstruktora dość regularnie, aby wyrazić różne koncepcje.

Rozważmy na przykład następującą klasę:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

Następnie możemy utworzyć podklasę:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

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.

Stephen
źródło
1

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 DatabaseEnginei ConnectionStringzostały ponownie zaimplementowane przez podklasę, która Configurationmiał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.

Keith
źródło
0

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.

Michael Brown
źródło