Mam problem projektowy dotyczący właściwości .NET.
interface IX
{
Guid Id { get; }
bool IsInvalidated { get; }
void Invalidate();
}
Problem:
Ten interfejs ma dwie właściwości tylko do odczytu Id
i IsInvalidated
. Jednak fakt, że są one tylko do odczytu, sam w sobie nie gwarantuje, że ich wartości pozostaną stałe.
Powiedzmy, że moim zamiarem było wyjaśnienie, że…
Id
reprezentuje stałą wartość (która dlatego może być bezpiecznie buforowana), natomiastIsInvalidated
może zmienić swoją wartość w czasie życiaIX
obiektu (i dlatego nie powinien być buforowany).
Jak mogę zmodyfikować, interface IX
aby umowa była wystarczająco wyraźna?
Moje trzy próby rozwiązania:
Interfejs jest już dobrze zaprojektowany. Obecność wywoływanej metody
Invalidate()
pozwala programiście wnioskować, żeIsInvalidated
może mieć na nią wpływ wartość właściwości o podobnej nazwie .Ten argument obowiązuje tylko w przypadkach, w których metoda i właściwość są podobnie nazwane.
Rozszerz ten interfejs o wydarzenie
IsInvalidatedChanged
:bool IsInvalidated { get; } event EventHandler IsInvalidatedChanged;
Obecność
…Changed
zdarzenia dlaIsInvalidated
oznacza, że ta właściwość może zmienić swoją wartość, a brak podobnego zdarzenia dlaId
jest obietnicą, że ta właściwość nie zmieni swojej wartości.Podoba mi się to rozwiązanie, ale jest wiele dodatkowych rzeczy, które w ogóle mogą się nie przydać.
Zastąp właściwość
IsInvalidated
metodąIsInvalidated()
:bool IsInvalidated();
To może być zbyt subtelna zmiana. Powinna to być wskazówka, że wartość jest obliczana świeżo za każdym razem - co nie byłoby konieczne, gdyby była stała. W temacie MSDN „Wybieranie właściwości i metod” można o tym powiedzieć:
Użyj metody, a nie właściwości, w następujących sytuacjach. […] Operacja zwraca inny wynik przy każdym wywołaniu, nawet jeśli parametry się nie zmieniają.
Jakich odpowiedzi oczekuję?
Najbardziej interesują mnie zupełnie inne rozwiązania problemu, wraz z wyjaśnieniem, w jaki sposób pokonały moje powyższe próby.
Jeśli moje próby są logicznie błędne lub mają znaczące wady, o których jeszcze nie wspomniano, tak, że pozostaje tylko jedno rozwiązanie (lub żadne), chciałbym usłyszeć o tym, gdzie popełniłem błąd.
Jeśli wady są niewielkie, a po ich uwzględnieniu pozostaje więcej niż jedno rozwiązanie, prosimy o komentarz.
Przynajmniej chciałbym uzyskać informacje zwrotne na temat tego, które jest Twoim preferowanym rozwiązaniem iz jakiego powodu (powodów).
źródło
IsInvalidated
potrzebujeprivate set
?class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
InvalidStateException
s, ale nie jestem pewien, czy jest to nawet teoretycznie możliwe. Byłoby jednak miło.Odpowiedzi:
Wolałbym rozwiązanie 3 niż 1 i 2.
Mój problem z rozwiązaniem 1 brzmi : co się stanie, jeśli nie ma
Invalidate
metody? Załóżmy interfejs zIsValid
właściwością, która zwracaDateTime.Now > MyExpirationDate
; możesz nie potrzebowaćSetInvalid
tutaj jawnej metody. Co jeśli metoda wpływa na wiele właściwości? Załóżmy, że typ połączenia ma właściwościIsOpen
iIsConnected
- na oba wpływ maClose
metoda.Rozwiązanie 2 : Jeśli jedynym punktem wydarzenia jest poinformowanie programistów, że właściwość o podobnej nazwie może zwracać różne wartości przy każdym wywołaniu, zdecydowanie odradzam to. Powinieneś utrzymywać krótkie i jasne interfejsy; co więcej, nie wszystkie wdrożenia mogą wywołać to zdarzenie. Ponownie wykorzystam mój
IsValid
przykład z góry: będziesz musiał wdrożyć Timer i wystrzelić zdarzenie, gdy dotrzeszMyExpirationDate
. W końcu, jeśli to zdarzenie jest częścią twojego publicznego interfejsu, użytkownicy interfejsu będą oczekiwać, że to wydarzenie zadziała.Mówiąc o tych rozwiązaniach: nie są złe. Obecność metody lub zdarzenia BĘDZIE wskazywać, że podobnie nazwana właściwość może zwracać różne wartości przy każdym wywołaniu. Mówię tylko, że same nie wystarczą, aby zawsze przekazać to znaczenie.
Rozwiązanie 3 jest tym, co chciałbym. Jak wspomniałem aviv, może to jednak działać tylko dla programistów C #. Dla mnie jako C # -dev fakt, że
IsInvalidated
nie jest własnością, natychmiast przekazuje znaczenie „to nie jest zwykły akcesor, coś tu się dzieje”. Nie dotyczy to jednak wszystkich i, jak wskazał MainMa, sama platforma .NET nie jest tutaj spójna.Jeśli chciałbyś skorzystać z rozwiązania 3, poleciłbym ogłosić, że jest to konwencja i niech cały zespół zastosuje się do niego. Uważam, że dokumentacja jest również niezbędna. Moim zdaniem podpowiedź przy zmienianiu wartości jest dość prosta: „ zwraca wartość true, jeśli wartość jest nadal ważna ”, „ wskazuje, czy połączenie zostało już otwarte ” vs. „ zwraca true, jeśli ten obiekt jest prawidłowy ”. W ogóle nie zaszkodziłoby to, by być bardziej wyraźnym w dokumentacji.
Tak więc moja rada brzmiałaby:
Zadeklaruj rozwiązanie 3 jako konwencję i konsekwentnie go przestrzegaj. Wyjaśnij w dokumentacji właściwości, czy właściwość ma zmieniające się wartości. Programiści pracujący z prostymi właściwościami poprawnie zakładają, że nie zmieniają się (tj. Zmieniają się tylko, gdy stan obiektu jest modyfikowany). Deweloperzy napotykają metodę, która brzmi jak to mogłoby być nieruchomość (
Count()
,Length()
,IsOpen()
) wie, że cos się dzieje i (miejmy nadzieję) przeczytać dokumenty metoda zrozumieć, co robi dokładnie sposób i jak się zachowuje.źródło
Istnieje czwarte rozwiązanie: polegać na dokumentacji .
Skąd wiesz, że ta
string
klasa jest niezmienna? Po prostu to wiesz, ponieważ przeczytałeś dokumentację MSDN.StringBuilder
z drugiej strony jest zmienna, ponieważ dokumentacja ci to mówi.Twoje trzecie rozwiązanie nie jest złe, ale .NET Framework go nie stosuje . Na przykład:
są właściwościami, ale nie oczekuj, że pozostaną one stałe za każdym razem.
To, co jest konsekwentnie stosowane w samej platformie .NET Framework, to:
jest metodą, podczas gdy:
jest własnością. W pierwszym przypadku robienie rzeczy może wymagać dodatkowej pracy, w tym na przykład zapytania do bazy danych. Ta dodatkowa praca może trochę potrwać . W przypadku nieruchomości oczekuje się, że potrwa to krótko : w rzeczywistości długość może się zmienić w trakcie życia listy, ale jej zwrot nie wymaga praktycznie nic.
W przypadku klas samo patrzenie na kod wyraźnie pokazuje, że wartość właściwości nie zmieni się w czasie życia obiektu. Na przykład,
jest jasne: cena pozostanie taka sama.
Niestety, nie można zastosować tego samego wzorca do interfejsu, a biorąc pod uwagę, że C # nie jest wystarczająco ekspresyjny w takim przypadku, dokumentacja (w tym Dokumentacja XML i diagramy UML) powinna zostać użyta do wypełnienia luki.
źródło
Lazy<T>.Value
obliczenie może zająć bardzo dużo czasu (gdy jest uzyskiwany po raz pierwszy).Moje 2 centy:
Opcja 3: jako nie-C # -er, nie byłby to żadna wskazówka; Jednak sądząc po cytacie, który przyniosłeś, biegli programiści C # powinni wiedzieć, że to coś znaczy. Nadal powinieneś jednak dodawać dokumenty na ten temat.
Opcja 2: Jeśli nie planujesz dodawać wydarzeń do wszystkich rzeczy, które mogą się zmienić, nie idź tam.
Inne opcje:
id
, która zwykle przyjmuje się, że jest niezmienna nawet w obiektach zmiennych.źródło
Inną opcją byłoby podzielenie interfejsu: zakładając, że po wywołaniu
Invalidate()
każdego wywołaniaIsInvalidated
powinna zwrócić tę samą wartośćtrue
, wydaje się, że nie ma powodu, aby wywoływaćIsInvalidated
tę samą część, która została wywołanaInvalidate()
.Sugeruję więc, abyś sprawdził unieważnienie lub spowodował je, ale wydaje się to nierozsądne. Dlatego sensowne jest zaoferowanie jednego interfejsu zawierającego
Invalidate()
operację do pierwszej części, a drugiego interfejsu do sprawdzania (IsInvalidated
) drugiej. Ponieważ z podpisu pierwszego interfejsu jest całkiem oczywiste, że spowoduje unieważnienie instancji, pozostałe pytanie (dotyczące drugiego interfejsu) jest rzeczywiście dość ogólne: jak określić, czy dany typ jest niezmienny .Wiem, że to nie odpowiada bezpośrednio na pytanie, ale przynajmniej sprowadza się do pytania, jak oznaczyć jakiś typ niezmienny , co jest cichym powszechnym i zrozumiałym problemem. Na przykład, zakładając, że wszystkie typy są zmienne, chyba że zdefiniowano inaczej, można wywnioskować, że drugi interfejs (
IsInvalidated
) jest zmienny i dlatego możeIsInvalidated
od czasu do czasu zmieniać wartość . Z drugiej strony załóżmy, że pierwszy interfejs wyglądał tak (pseudo-składnia):Ponieważ jest oznaczony jako niezmienny, wiesz, że identyfikator się nie zmieni. Oczywiście wywołanie nieważności spowoduje zmianę stanu instancji, ale zmiana ta nie będzie możliwa do zaobserwowania za pośrednictwem tego interfejsu.
źródło
[Immutable]
o ile obejmuje on metody, których jedynym celem jest spowodowanie mutacji. Chciałbym przenieśćInvalidate()
metodę doInvalidatable
interfejsu.Id
, otrzymasz tę samą wartość za każdym razem. To samo dotyczyInvalidate()
(nic nie dostajesz, ponieważ typem zwrotu jestvoid
). Dlatego ten typ jest niezmienny. Oczywiście będziesz mieć efekty uboczne , ale nie będziesz mógł ich zaobserwować przez interfejsIX
, więc nie będą miały żadnego wpływu na klientówIX
.IX
jest niezmienne. I że są to skutki uboczne; są po prostu rozmieszczone między dwoma podzielonymi interfejsami, tak że klienci nie muszą się tym przejmować (w przypadkuIX
) lub że jest oczywiste, jaki może być efekt uboczny (w przypadkuInvalidatable
). Ale dlaczego nie przenieśćInvalidate
się doInvalidatable
? To uczyniłobyIX
niezmiennym i czystym, iInvalidatable
nie stałoby się bardziej zmienne, ani bardziej nieczyste (ponieważ jest to już oba z nich).Invalidate()
iIsInvalidated
przez tego samego konsumenta interfejsu . Jeśli zadzwoniszInvalidate()
, powinieneś wiedzieć, że zostanie unieważniony, więc po co to sprawdzać? W ten sposób możesz zapewnić dwa różne interfejsy do różnych celów.