Opcjonalne parametry lub przeciążone konstruktory

28

Wdrażam a DelegateCommand, kiedy miałem zaimplementować konstruktora (-ów), zaproponowałem dwie następujące opcje projektu:

1: Posiadanie wielu przeciążonych konstruktorów

public DelegateCommand(Action<T> execute) : this(execute, null) { }

public DelegateCommand(Action<T> execute, Func<T, bool> canExecute)
{
    this.execute = execute;
    this.canExecute = canExecute;
}

2: Posiadanie tylko jednego konstruktora z opcjonalnym parametrem

public DelegateCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
    this.execute = execute;
    this.canExecute = canExecute;
}

Nie wiem, którego użyć, ponieważ nie wiem, jakie potencjalne zalety / wady wiążą się z jednym z dwóch proponowanych sposobów. Oba można nazwać tak:

var command = new DelegateCommand(this.myExecute);
var command2 = new DelegateCommand(this.myExecute, this.myCanExecute);

Czy ktoś może skierować mnie we właściwym kierunku i wyrazić opinię?

Thomas Flinkow
źródło
4
KISS i YAGNI. W przypadku wątpliwości należy wdrożyć oba. Po chwili przeszukaj odniesienia do obu konstruktorów. Nie zaskoczyłoby mnie, gdyby jedno z nich nigdy nie zostało skonsumowane na zewnątrz. Lub marginalnie zużyty. Jest to coś łatwego do znalezienia przy wykonywaniu analizy statycznej kodu.
Laiv
1
Konstruktorzy statyczni (podobni Bitmap.FromFile) są również opcją
BlueRaja - Danny Pflughoeft
3
Zachowaj regułę analizy kodu CA1026: Nie należy używać parametrów domyślnych .
Dan Lyons
2
@Laiv, brakuje mi czegoś? Są używane w ten sam sposób, a zatem mają taki sam podpis. Nie możesz wyszukiwać referencji i powiedzieć, o czym dzwonił rozmówca. To brzmi bardziej, jakbyś miał na myśli to, czy OP powinien mieć nawet ten drugi argument, ale nie o to pyta OP (i może dobrze wiedzieć, że czasami potrzebują obu, dlatego pytają).
Kat
2
@ Voo Kompilator programu Openedge | Progress 10.2B ignoruje je. Niemal zabiło naszą niezłomną strategię zmiany metody; zamiast tego musieliśmy użyć przeciążenia dla tego samego efektu.
Bret

Odpowiedzi:

24

Wolę wiele konstruktorów niż wartości domyślne i osobiście nie podoba mi się twój przykład dwóch konstruktorów, powinien być zaimplementowany inaczej.

Powodem używania wielu konstruktorów jest to, że główny może po prostu sprawdzić, czy wszystkie parametry nie są zerowe i czy są poprawne, podczas gdy inne konstruktory mogą podać wartości domyślne dla głównego.

Jednak w twoich przykładach nie ma między nimi żadnej różnicy, ponieważ nawet drugorzędny konstruktor nullpodaje wartość domyślną, a główny konstruktor musi również znać wartość domyślną. Myślę, że nie powinno.

Oznacza to, że byłby czystszy i lepiej oddzielony, gdyby został wdrożony w ten sposób:

public DelegateCommand(Action<T> execute) : this(execute, _ => true) { }

public DelegateCommand(Action<T> execute, Func<T, bool> canExecute)
{
    this.execute = execute ?? throw new ArgumentNullException(..);
    this.canExecute = canExecute ?? throw new ArgumentNullException(..);
}

zwróć uwagę _ => truena główny konstruktor, który teraz sprawdza również wszystkie parametry nulli nie przejmuje się żadnymi wartościami domyślnymi.


Najważniejsza jest jednak rozszerzalność. Wiele konstruktorów jest bezpieczniejszych, gdy istnieje możliwość, że rozszerzysz swój kod w przyszłości. Jeśli dodasz więcej wymaganych parametrów, a opcjonalne muszą pojawić się na końcu, zepsujesz wszystkie obecne implementacje. Możesz zrobić starego konstruktora [Obsolete]i poinformować użytkowników, że zostanie on usunięty, dając im czas na migrację do nowej implementacji bez natychmiastowego łamania kodu.


Z drugiej strony włączenie zbyt wielu parametrów jako opcjonalnych byłoby również mylące, ponieważ jeśli niektóre z nich są wymagane w jednym scenariuszu, a opcjonalne w innym, trzeba by studiować dokumentację zamiast wybierać odpowiedniego konstruktora, po prostu patrząc na jego parametry.

t3chb0t
źródło
12
Z drugiej strony włączenie zbyt wielu parametrów jako opcjonalnych byłoby mylące ... Szczerze mówiąc, posiadanie zbyt wielu parametrów (niezależnie od tego, czy są opcjonalne czy nie) jest mylące.
Andy
1
@DavidPacker Zgadzam się z tobą, ale myślę, że ile parametrów jest za dużo, to inna historia, myślę ;-)
t3chb0t
2
Jest jeszcze jedna różnica między posiadaniem konstruktora, który pomija parametr, a konstruktorem, który ma wartość domyślną dla parametru. W pierwszym przypadku osoba dzwoniąca może wyraźnie uniknąć przekazania parametru, podczas gdy w drugim przypadku osoba dzwoniąca nie może - ponieważ przekazanie żadnego parametru nie jest równoznaczne z przekazaniem wartości domyślnej. Jeśli konstruktor musi wprowadzić to rozróżnienie, jedyną drogą jest dwa konstruktory.
Gary McGill
1
Załóżmy na przykład, że mam klasę, która formatuje ciągi, które opcjonalnie mogę zbudować za pomocą CultureInfoobiektu. Wolałbym API, które pozwoliło CultureInfoparametr mają być dostarczone poprzez dodatkowy parametr, ale podkreślił, że gdyby to było dostarczane to powinno nie być null. W ten sposób przypadkowe nullwartości nie zostaną źle zinterpretowane jako „brak”.
Gary McGill
Nie jestem pewien, czy rozszerzalność jest tak naprawdę powodem dla opcjonalnych parametrów; jeśli w ogóle masz wymagane parametry i kiedykolwiek zmienisz ich liczbę, będziesz musiał utworzyć nowego konstruktora i Obsoletestarego, jeśli chcesz uniknąć zepsucia, niezależnie od tego, czy masz parametry opcjonalne, czy nie. Z opcjonalnymi parametrami musisz tylko je Obsoleteusunąć.
Herohtar
17

Biorąc pod uwagę, że jedyne, co robisz w konstruktorze, to proste przypisanie, rozwiązanie dla pojedynczego konstruktora może być lepszym wyborem w twoim przypadku. Drugi konstruktor nie zapewnia żadnej dodatkowej funkcjonalności i z projektu konstruktora z dwoma parametrami jasno wynika, że ​​drugi argument nie musi być podawany.

Wiele konstruktorów ma sens, gdy budujesz obiekt z różnych typów. W takim przypadku konstruktory zastępują zewnętrzną fabrykę, ponieważ przetwarzają parametry wejściowe i formatują je do poprawnej wewnętrznej reprezentacji właściwości budowanej klasy. W twoim przypadku tak się jednak nie dzieje, dlatego jeden konstruktor powinien być wystarczający.

Andy
źródło
3

Myślę, że są dwa różne przypadki, w których każde podejście może zabłysnąć.

Po pierwsze, kiedy mamy proste konstruktory (co z mojego doświadczenia zwykle tak jest), rozważyłbym opcjonalne argumenty.

  1. Minimalizują kod, który musi zostać napisany (a zatem również, który musi zostać odczytany).
  2. Możesz upewnić się, że dokumentacja jest w jednym miejscu i nie jest powtarzana (co jest złe, ponieważ otwiera dodatkowy obszar, który może stać się nieaktualny).
  3. Jeśli masz wiele opcjonalnych argumentów, możesz uniknąć bardzo mylącego zestawu kombinacji konstruktorów. Heck, nawet z tylko 2 opcjonalnymi argumentami (niezwiązanymi ze sobą), jeśli chcesz osobnych, przeciążonych konstruktorów, musisz mieć 4 konstruktory (wersja bez żadnego, wersja z każdym i wersja z obydwoma). To oczywiście nie jest dobrze skalowane.

Buuuut, są przypadki, w których opcjonalne argumenty w konstruktorach sprawiają, że sprawy stają się bardziej skomplikowane. Oczywistym przykładem jest to, że te opcjonalne argumenty są wyłączne (tzn. Nie można ich używać razem). Przeciążenie konstruktorów, które zezwalają tylko na rzeczywiście poprawne kombinacje argumentów, zapewniłoby wymuszenie kompilacji tego ograniczenia w czasie. To powiedziawszy, powinieneś także unikać tego przypadku (np. Z wieloma klasami dziedziczącymi z bazy, gdzie każda klasa zachowuje się wyłącznie na zasadzie wyłączności).

Kat
źródło
+1 za wzmiankę o wybuchu permutacji z wieloma opcjonalnymi parametrami.
Jpsy