Czy warto używać konstruktorów i płynnych interfejsów z inicjatorami obiektów?

10

W Javie i C # można utworzyć obiekt z właściwościami, które można ustawić przy inicjalizacji, poprzez zdefiniowanie konstruktora za pomocą parametrów, zdefiniowanie każdej właściwości po skonstruowaniu obiektu lub użycie wzorca interfejsu konstruktora / płynu. Jednak C # 3 wprowadził inicjatory obiektów i kolekcji, co oznaczało, że wzorzec konstruktora był w dużej mierze bezużyteczny. W języku bez inicjatorów można zaimplementować konstruktor, a następnie użyć go w następujący sposób:

Vehicle v = new Vehicle.Builder()
                    .manufacturer("Toyota")
                    .model("Camry")
                    .year(1997)
                    .colour(CarColours.Red)
                    .addSpecialFeature(new Feature.CDPlayer())
                    .addSpecialFeature(new Feature.SeatWarmer(4))
                    .build();

I odwrotnie, w języku C # można napisać:

var vehicle = new Vehicle {
                Manufacturer = "Toyota",
                Model = "Camry",
                Year = 1997,
                Colour = CarColours.Red,
                SpecialFeatures = new List<SpecialFeature> {
                    new Feature.CDPlayer(),
                    new Feature.SeatWarmer { Seats = 4 }
                }
              }

... eliminując potrzebę konstruktora, jak widać w poprzednim przykładzie.

Czy na podstawie tych przykładów konstruktory są nadal przydatne w języku C #, czy też zostały całkowicie zastąpione przez inicjatory?

svbnet
źródło
are builders usefuldlaczego ciągle widzę tego rodzaju pytania? Builder to wzorzec projektowy - na pewno może zostać ujawniony jako funkcja językowa, ale na koniec jest to tylko wzorzec projektowy. Wzorzec projektowy nie może być „zły”. Może, ale nie musi spełnić twojego przypadku użycia, ale to nie czyni całego wzoru błędnym, tylko jego zastosowanie. Pamiętaj, że pod koniec dnia wzorce projektowe rozwiązują określone problemy - jeśli nie napotykasz problemu, to dlaczego miałbyś go rozwiązać?
VLAZ
@vlaz Nie twierdzę, że budowniczowie się mylą - w rzeczywistości moje pytanie dotyczyło tego, czy istnieją jakieś przypadki użycia dla konstruktorów, ponieważ inicjatory są implementacją najczęstszego przypadku, w którym można użyć konstruktorów. Oczywiście ludzie odpowiedzieli, że konstruktor jest nadal przydatny do ustawiania prywatnych pól, co z kolei odpowiedziało na moje pytanie.
svbnet
3
@vlaz Aby wyjaśnić: mówię, że inicjatory w dużej mierze zastąpiły „tradycyjny” wzorzec interfejsu konstruktora / fluidu polegający na pisaniu wewnętrznej klasy konstruktora, a następnie pisaniu metod ustawiających łańcuch w tej klasie, które tworzą nowe wystąpienie tej klasy nadrzędnej. Ja nie mówiąc, że incjalizatory są zamiennikiem dla danego budowniczy; Mówię, że inicjatory oszczędzają programistom konieczności implementowania tego konkretnego wzorca konstruktora, z wyjątkiem przypadków użycia wymienionych w odpowiedziach, co nie czyni wzorca konstruktora bezużytecznym.
svbnet
5
@vlaz Nie rozumiem twojej obawy. „Mówiłeś, że wzór nie jest użyteczny” - nie, Joe pytał, czy wzór jest przydatny. Jak to jest złe, złe lub błędne pytanie? Czy uważasz, że odpowiedź jest oczywista? Nie sądzę, aby odpowiedź była oczywista; wydaje mi się, że to dobre pytanie.
Tanner Swett
2
@vlaz, wzorce singletonu i lokalizatora usług są nieprawidłowe. Wzory projektowe Ergo mogą być niepoprawne.
David Arno

Odpowiedzi:

12

Jak porusza @ user248215, prawdziwym problemem jest niezmienność. Powodem użycia konstruktora w języku C # byłoby zachowanie jawności inicjalizatora bez konieczności ujawniania ustawialnych właściwości. To nie jest kwestia enkapsulacji, dlatego napisałem własną odpowiedź. Hermetyzacja jest dość ortogonalna, ponieważ wywoływanie settera nie implikuje tego, co faktycznie robi setter, ani nie wiąże cię z jego implementacją.

Następna wersja C #, 8.0, prawdopodobnie wprowadzi withsłowo kluczowe, które pozwoli na jasne i zwięzłe zainicjowanie niezmiennych obiektów bez konieczności pisania programów budujących.

Inną interesującą rzeczą, którą możesz zrobić z konstruktorami, w przeciwieństwie do inicjalizatorów, jest to, że mogą one dawać różne typy obiektów w zależności od sekwencji wywoływanych metod.

Na przykład

value.Match()
    .Case((DateTime d) => Console.WriteLine($"{d: yyyy-mm-dd}"))
    .Case((double d) => Console.WriteLine(Math.Round(d, 4));
    // void

var str = value.Match()
    .Case((DateTime d) => $"{d: yyyy-mm-dd}")
    .Case((double d) => Math.Round(d, 4).ToString())
    .ResultOrDefault(string.Empty);
    // string

Aby wyjaśnić powyższy przykład, jest to biblioteka dopasowywania wzorców, która używa wzorca konstruktora do zbudowania „dopasowania” poprzez określenie przypadków. Przypadki są dodawane przez wywołanie Casemetody przekazującej jej funkcję. Jeśli valuemożna go przypisać do typu parametru funkcji, jest on wywoływany. Pełny kod źródłowy można znaleźć na GitHub, a ponieważ komentarze XML są trudne do odczytania zwykłym tekstem, oto link do dokumentacji zbudowanej przez SandCastle (zobacz sekcję Uwagi )

Aluan Haddad
źródło
Nie widzę, jak nie można tego zrobić, inicjując obiekt za pomocą IEnumerable<Func<T, TResult>>elementu.
Caleth,
@Caleth To było coś, z czym eksperymentowałem, ale są pewne problemy z tym podejściem. Nie dopuszcza klauzul warunkowych (nie pokazano, ale użyto i wykazano w dokumentacji powiązanej) i nie pozwala na wnioskowanie typu TResult. Ostatecznie wnioskowanie typu miało z tym wiele wspólnego. Chciałem też, żeby „wyglądała” jak struktura kontrolna. Nie chciałem też używać inicjatora, ponieważ pozwoliłoby to na mutację.
Aluan Haddad,
12

Inicjatory obiektów wymagają, aby właściwości były dostępne dla kodu wywołującego. Zagnieżdżeni konstruktorzy mogą uzyskiwać dostęp do prywatnych członków klasy.

Jeśli chcesz Vehicleustawić wartość niezmienną (poprzez uczynienie wszystkich ustawiających prywatnymi), wówczas można użyć zagnieżdżonego konstruktora do ustawienia zmiennych prywatnych.

użytkownik248215
źródło
0

Wszystkie służą innym celom !!!

Konstruktory mogą inicjować pola, które są oznaczone, readonlya także członków prywatnych i chronionych. Masz jednak ograniczone możliwości konstruktora; powinieneś na przykład unikać przechodzenia thisdo jakichkolwiek zewnętrznych metod i powinieneś unikać wywoływania wirtualnych członków, ponieważ mogą one być wykonywane w kontekście klasy pochodnej, która jeszcze się nie skonstruowała. Ponadto konstruktory mają zagwarantowane działanie (chyba że program wywołujący robi coś bardzo nietypowego), więc jeśli ustawisz pola w konstruktorze, kod reszty klasy może założyć, że pola te nie będą miały wartości zerowej.

Inicjatory uruchamiane są po zakończeniu budowy. Pozwalają tylko wywoływać nieruchomości publiczne; pola prywatne i / lub tylko do odczytu nie mogą być ustawione. Zgodnie z konwencją czynność ustanawiania właściwości powinna być dość ograniczona, np. Powinna być idempotentna i mieć ograniczone skutki uboczne.

Metody konstruktora są rzeczywistymi metodami i dlatego pozwalają na więcej niż jeden argument, a zgodnie z konwencją mogą mieć skutki uboczne, w tym tworzenie obiektów. Chociaż nie mogą ustawiać pól tylko do odczytu, mogą właściwie robić cokolwiek innego. Ponadto metody mogą być implementowane jako metody rozszerzeń (podobnie jak prawie cała funkcjonalność LINQ).

John Wu
źródło