Czy jest to anty-wzorzec, jeśli właściwość klasy tworzy i zwraca nową instancję klasy?

24

Mam klasę o nazwie, Headingktóra robi kilka rzeczy, ale powinna również być w stanie zwrócić przeciwieństwo bieżącej wartości nagłówka, która w końcu musi zostać użyta poprzez utworzenie nowej instancji Headingsamej klasy.

Mogę mieć prostą właściwość wywoływaną w reciprocalcelu zwrócenia przeciwnego nagłówka bieżącej wartości, a następnie ręcznie utworzyć nową instancję klasy Heading, lub mogę utworzyć metodę polegającą createReciprocalHeading()na automatycznym utworzeniu nowej instancji klasy Heading i zwróceniu jej do właściwości użytkownik.

Jednak jeden z moich kolegów polecił mi po prostu utworzenie właściwości klasy o nazwie, reciprocalktóra zwraca nową instancję samej klasy za pomocą metody getter.

Moje pytanie brzmi: czy to nie jest anty-wzorzec dla właściwości klasy, która zachowuje się w ten sposób?

Uważam to za mniej intuicyjne, ponieważ:

  1. Moim zdaniem własność klasy nie powinna zwracać nowej instancji klasy, i
  2. Nazwa właściwości, która reciprocalnie pomaga deweloperowi w pełni zrozumieć jej zachowanie, bez uzyskania pomocy z IDE lub sprawdzenia podpisu gettera.

Czy jestem zbyt rygorystyczny co do tego, co powinna zrobić własność klasy, czy jest to uzasadniona obawa? Zawsze starałem się zarządzać stanem klasy poprzez jej pola i właściwości oraz jej zachowanie za pomocą metod, i nie rozumiem, jak to pasuje do definicji właściwości klasy.

53777A
źródło
6
Jak powiedział @Ewan w ostatnim akapicie swojej odpowiedzi, daleki od bycia anty-wzorem, posiadanie Headingjako niezmiennego typu i reciprocalpowrót nowego Headingjest dobrą praktyką „dołu sukcesu”. (z zastrzeżeniem, że dwa wezwania reciprocalpowinny zwrócić „to samo”, tzn. powinny przejść test równości.)
David Arno,
20
„moja klasa nie jest niezmienna”. Tam jest twój prawdziwy anty-wzór. ;)
David Arno,
3
@DavidArno Cóż, czasami istnieje ogromna kara wydajności za uczynienie pewnych rzeczy niezmiennymi, szczególnie jeśli język nie obsługuje ich natywnie, ale w przeciwnym razie zgadzam się, że niezmienność jest dobrą praktyką w wielu przypadkach.
53777A,
2
@corking klasa jest zaimplementowana zarówno w języku C #, jak i TypeScript, ale raczej nie zwracam uwagi na ten język.
53777A,
2
@Kaiserludi brzmi jak odpowiedź (świetna odpowiedź) - komentarze mają na celu
domaganie się

Odpowiedzi:

22

Nie jest nieznane posiadanie takich funkcji jak Copy () lub Clone (), ale tak, myślę, że masz rację, że martwisz się tym.

Weź na przykład:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

Przydałoby się ostrzeżenie, że za każdym razem właściwość była nowym obiektem.

możesz również założyć, że:

h2.reciprocal == h1

Jednak. Jeśli twoja klasa nagłówkowa była niezmiennym typem wartości, to możesz zaimplementować te relacje, a odwrotność może być dobrą nazwą dla operacji

Ewan
źródło
1
Wystarczy pamiętać, że Copy()i Clone()są sposoby, a nie właściwości. Uważam, że OP odnosi się do osób uzyskujących nieruchomości zwracających nowe wystąpienia.
Lynn,
6
wiem. ale właściwości są (w pewnym sensie) także metodami w c #
Ewan
4
W takim przypadku, C # ma znacznie więcej do zaoferowania: String.Substring(), Array.Reverse(), Int32.GetHashCode(), itd.
Lynn
If your heading class was an immutable value type- Myślę, że powinieneś dodać, że ustawiacze właściwości na obiektach niezmiennych powinni zawsze zwracać nowe wystąpienia (lub przynajmniej jeśli nowa wartość nie jest taka sama jak stara wartość), ponieważ taka jest definicja obiektów niezmiennych. :)
Ivan Kolmychek
17

Interfejs klasy prowadzi użytkownika do przyjęcia założeń dotyczących jego działania.

Jeśli wiele z tych założeń jest poprawnych, a kilka jest błędnych, interfejs jest dobry.

Jeśli wiele z tych założeń jest błędnych, a niewiele ma rację, interfejs jest śmieci.

Powszechnym założeniem dotyczącym właściwości jest to, że wywoływanie funkcji get jest tanie. Innym powszechnym założeniem dotyczącym właściwości jest to, że wywołanie funkcji get dwa razy z rzędu zwróci to samo.


Możesz obejść ten problem, stosując spójność, aby zmienić oczekiwania. Na przykład, dla małej biblioteki 3D, gdzie trzeba Vector, Ray Matrixitd można zrobić to w taki sposób, jak w pozyskiwaniu Vector.normali Matrix.inversesą prawie zawsze drogie. Niestety, nawet jeśli twoje interfejsy konsekwentnie używają drogich właściwości, interfejsy będą co najmniej tak intuicyjne, jak te, które używają, Vector.create_normal()a Matrix.create_inverse()jednak nie znam mocnego argumentu, który można by uzasadnić, że używanie właściwości tworzy bardziej intuicyjny interfejs, nawet po zmianie oczekiwań.

Peter - Unban Robert Harvey
źródło
10

Właściwości powinny zwracać się bardzo szybko, zwracać tę samą wartość przy powtarzanych wywołaniach, a uzyskanie ich wartości nie powinno mieć skutków ubocznych. To, co tu opisujesz, nie powinno być implementowane jako własność.

Twoja intuicja jest zasadniczo poprawna. Metoda fabryczna nie zwraca tej samej wartości przy powtarzanych wywołaniach. Za każdym razem zwraca nową instancję. To prawie dyskwalifikuje to jako własność. Nie jest to również szybkie, jeśli późniejszy rozwój zwiększa wagę tworzenia instancji, np. Zależności sieci.

Właściwości powinny zasadniczo być bardzo prostymi operacjami, które pobierają / ustawiają część bieżącego stanu klasy. Operacje fabryczne nie spełniają tych kryteriów.

Brad Thomas
źródło
1
DateTime.NowWłaściwość jest powszechnie stosowane i odnosi się do nowego obiektu. Myślę, że nazwa właściwości może pomóc w usunięciu niejasności co do tego, czy ten sam obiekt jest zwracany za każdym razem. Wszystko jest w nazwie i w jaki sposób jest używane.
Greg Burghardt,
3
DateTime.Now jest uważany za błąd. Zobacz stackoverflow.com/questions/5437972/…
Brad Thomas
@GregBurghardt Efekt uboczny nowego obiektu może sam w sobie nie być problemem, ponieważ DateTimejest to typ wartości i nie ma pojęcia tożsamości obiektu. Jeśli dobrze rozumiem, problem polega na tym, że nie jest deterministyczny i za każdym razem zwraca nową wartość.
Zev Spitz,
1
@GregBurghardt DateTime.Newjest statyczny i dlatego nie można oczekiwać, że będzie reprezentował stan obiektu.
Tsahi Asher,
5

Nie sądzę, aby istniała odpowiedź na to pytanie niezależnie od języka, ponieważ to, co stanowi „właściwość”, jest pytaniem specyficznym dla danego języka, a to, czego oczekuje osoba wywołująca „właściwość”, jest również pytaniem specyficznym dla danego języka. Myślę, że najbardziej owocnym sposobem na myślenie o tym jest myślenie o tym, jak to wygląda z punktu widzenia dzwoniącego.

W języku C # właściwości wyróżniają się tym, że są (konwencjonalnie) pisane wielkimi literami (jak metody), ale nie zawierają nawiasów (jak zmienne instancji publicznej). Jeśli widzisz następujący kod, brak dokumentacji, czego oczekujesz?

var reciprocalHeading = myHeading.Reciprocal;

Jako względny nowicjusz w języku C #, ale czytający Wytyczne użytkowania nieruchomości firmy Microsoft , spodziewałbym Reciprocalsię między innymi:

  1. być logicznym członkiem Headingklasy
  2. bądź niedrogi, aby zadzwonić, aby nie trzeba było buforować wartości
  3. brak obserwowalnych skutków ubocznych
  4. dają ten sam wynik, jeśli zostaną wywołane dwa razy pod rząd
  5. (może) zaoferować ReciprocalChangedwydarzenie

Spośród tych założeń (3) i (4) są prawdopodobnie poprawne (zakładając, że Headingjest to niezmienny typ wartości, jak w odpowiedzi Ewana ), (1) jest dyskusyjny, (2) jest nieznany, ale także dyskusyjny, a (5) jest mało prawdopodobne mają sens semantyczny (chociaż cokolwiek ma nagłówek, może być HeadingChangedzdarzeniem). To sugeruje mi, że w API C # „pobierz lub oblicz wzajemność” nie powinno być implementowane jako właściwość, ale zwłaszcza jeśli obliczenia są tanie i Headingsą niezmienne, jest to przypadek graniczny.

(Zauważ jednak, że żaden z tych problemów nie ma nic wspólnego z tym, czy wywołanie właściwości tworzy nową instancję , nawet (2). Tworzenie obiektów w CLR samo w sobie jest dość tanie).

W Javie właściwości są konwencją nazewnictwa metod. Jeśli widzę

Heading reciprocalHeading = myHeading.getReciprocal();

moje oczekiwania są podobne do powyższych (jeśli mniej wyraźnie określone): oczekuję, że połączenie będzie tanie, idempotentne i pozbawione skutków ubocznych. Jednak poza ramą JavaBeans koncepcja „właściwości” nie jest tak bardzo znacząca w Javie, a zwłaszcza, gdy rozważa się niezmienną właściwość bez odpowiadającej jej setReciprocal(), getXXX()konwencja jest obecnie nieco staromodna. Od Effective Java , drugie wydanie (już ponad osiem lat):

Metody zwracające booleanfunkcję lub atrybut obiektu, na który są wywoływane, są zwykle nazywane rzeczownikiem, wyrażeniem rzeczownikowym lub wyrażeniem czasownikowym rozpoczynającym się od czasownika get… Istnieje kontyngent wokalny, który twierdzi, że tylko trzecia forma (zaczynająca się od get) jest akceptowalna, ale jest niewiele podstaw do tego twierdzenia. Pierwsze dwie formy zwykle prowadzą do bardziej czytelnego kodu… (s. 239)

Zatem we współczesnym, bardziej płynnym API spodziewałbym się zobaczyć

Heading reciprocalHeading = myHeading.reciprocal();

- co ponownie sugeruje, że wywołanie jest tanie, idempotentne i nie ma skutków ubocznych, ale nie powiedziałoby nic o tym, czy wykonywane jest nowe obliczenie, czy tworzony jest nowy obiekt. To jest w porządku; w dobrym API, nie powinno mnie to obchodzić.

W Ruby nie ma czegoś takiego jak własność. Istnieją „atrybuty”, ale jeśli widzę

reciprocalHeading = my_heading.reciprocal

Nie mam bezpośredniego sposobu, aby dowiedzieć się, czy uzyskuję dostęp do zmiennej instancji @reciprocalza attr_readerpomocą prostej metody akcesorium, czy też wywołuję metodę, która wykonuje kosztowne obliczenia. Jednak fakt, że nazwa metody jest rzeczownikiem prostym, zamiast powiedzieć calcReciprocal, sugeruje ponownie, że wywołanie jest co najmniej tanie i prawdopodobnie nie ma skutków ubocznych.

W Scali konwencja nazewnictwa mówi, że metody z efektami ubocznymi przyjmują nawiasy, a metody bez nich nie, ale

val reciprocal = heading.reciprocal

może być jednym z:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Zauważ, że Scala dopuszcza różne rzeczy, które mimo wszystko są zniechęcane przez przewodnik po stylu . To jedna z moich głównych irytacji ze Scali.)

Brak nawiasów mówi mi, że połączenie nie ma skutków ubocznych; nazwa ponownie sugeruje, że połączenie powinno być stosunkowo tanie. Poza tym nie obchodzi mnie, w jaki sposób zyskuje mi wartość.

W skrócie: poznaj język, którego używasz, i dowiedz się, jakie oczekiwania przyniosą inni programiści w interfejsie API. Cała reszta to szczegół implementacji.

David Moles
źródło
1
Daj +1 jednej z moich ulubionych odpowiedzi, dziękuję za poświęcenie czasu na napisanie tego.
53777A,
2

Jak powiedzieli inni, zwracanie instancji tej samej klasy jest dość powszechnym wzorcem.

Nazewnictwo powinno iść w parze z konwencjami nazewnictwa języka.

Na przykład w Javie prawdopodobnie spodziewałbym się, że zostanie wywołany getReciprocal();

Biorąc to pod uwagę, rozważę obiekty zmienne vs niezmienne .

W przypadku tych niezmiennych rzeczy są dość łatwe i nie zaszkodzi, jeśli zwrócisz ten sam przedmiot, czy nie.

W przypadku zmiennych może to być bardzo przerażające.

b = a.reciprocal
a += 1
b = what?

Teraz do czego odnosi się b? Odwrotność pierwotnej wartości? Czy odwrotność zmienionego? To może być zbyt krótki przykład, ale rozumiesz.

W takich przypadkach szukaj lepszego nazewnictwa, które informuje o tym, co się dzieje, na przykład createReciprocal()może być lepszym wyborem.

Ale tak naprawdę zależy to również od kontekstu.

Eiko
źródło
Miałeś na myśli mutable ?
Carsten S
@CarstenS Tak, oczywiście, dzięki. Nie mam pojęcia, gdzie była moja głowa. :)
Eiko
Nie zgadzam się trochę dalej getReciprocal(), ponieważ to „brzmi” jak „normalny” getter stylu JavaBeans. Preferuj „createXXX ()” lub ewentualnie „obliczyćXXX ()”, które wskazują lub sugerują innym programistom, że dzieje się coś innego
user949300,
@ user949300 Zgadzam się nieco z twoimi obawami - i wspomniałem o nazwie create ... poniżej. Ale są też wady. Utwórz ... oznacza nowe przypadki, które nie zawsze mogą mieć miejsce (pomyśl o osobnych obiektach dedykowanych dla niektórych specjalnych przypadków), oblicz ... rodzaj szczegółów wycieków - które mogą zachęcić do błędnych założeń dotyczących ceny.
Eiko
1

W trosce o pojedynczą odpowiedzialność i przejrzystość chciałbym mieć ReverseHeadingFactory, który wziąłby obiekt i zwrócił jego odwrotność. Ułatwiłoby to wyjaśnienie, że zwracany jest nowy obiekt, i oznaczałoby, że kod generujący odwrotność został zamknięty w innym kodzie.

matowy freake
źródło
1
+1 za wyrazistość nazwy, ale co jest złego w SRP w innych rozwiązaniach?
53777A,
3
Dlaczego polecasz klasę fabryczną zamiast metody fabrycznej? (Moim zdaniem użyteczną odpowiedzialnością obiektu jest często emitowanie obiektów podobnych do niego, takich jak iterator.next. Czy to łagodne naruszenie SRP powoduje inne problemy z inżynierią oprogramowania?)
dorkork
2
@orkorking Klasa fabryczna kontra metoda (moim zdaniem) to zwykle ten sam typ pytania, co: „Czy obiekt jest porównywalny, czy powinienem zrobić komparator?” Tworzenie komparatora jest zawsze w porządku , ale tylko w porządku, aby były porównywalne, jeśli jest to dość uniwersalny sposób ich porównywania. (Np. Większe liczby są większe niż mniejsze, jest to dobre dla porównywalnych, ale można powiedzieć, że wszystkie wartości są większe niż wszystkie szanse, zrobiłbym to z porównania) - Więc w tym przypadku sądzę, że jest tylko jeden sposób na odwrócenie nagłówka, więc skłaniam się ku stworzeniu metody pozwalającej uniknąć dodatkowej klasy.
Captain Man,
0

To zależy. Zwykle oczekuję, że właściwości zwrócą rzeczy, które są częścią instancji, więc zwrócenie innej instancji tej samej klasy byłoby nieco niezwykłe.

Ale na przykład klasa łańcuchowa może mieć właściwości „firstWord”, „lastWord” lub jeśli obsługuje Unicode „firstLetter”, „lastLetter”, które byłyby obiektami pełnego ciągu - zwykle mniejszymi.

gnasher729
źródło
0

Ponieważ pozostałe odpowiedzi dotyczą części Właściwość, zajmę się twoim drugim tematem reciprocal :

Nie używaj niczego innego niż reciprocal. Jest to jedyny poprawny termin na to, co opisujesz (i jest to termin formalny). Nie pisz niepoprawnego oprogramowania, aby uchronić programistów przed nauczeniem się domeny, w której pracują. W nawigacji terminy mają bardzo konkretne znaczenie i używanie czegoś tak pozornie nieszkodliwego, jak reversei oppositemoże prowadzić do zamieszania w niektórych kontekstach.

Shaz
źródło
0

Logicznie nie, nie jest to właściwość nagłówka. Gdyby tak było, można by również powiedzieć, że ujemna liczba jest własnością tej liczby, i szczerze mówiąc, że „własność” straci wszelkie znaczenie, prawie wszystko byłoby własnością.

Odwrotność jest czystą funkcją nagłówka, tak samo jak ujemna liczba jest czystą funkcją tej liczby.

Zasadniczo, jeśli ustawienie nie ma sensu, prawdopodobnie nie powinno być własnością. Ustawienie to nadal może być oczywiście zabronione, ale dodanie settera powinno być teoretycznie możliwe i sensowne.

Teraz, w niektórych językach, sensowne może być posiadanie go jako właściwości określonej w kontekście tego języka, jeśli przynosi pewne korzyści wynikające z mechaniki tego języka. Na przykład, jeśli chcesz przechowywać go w bazie danych, prawdopodobnie rozsądnym rozwiązaniem jest ustawienie go jako właściwości, jeśli pozwala on na automatyczną obsługę przez środowisko ORM. A może jest to język, w którym nie ma rozróżnienia między właściwością a parametryczną funkcją składową (nie znam takiego języka, ale jestem pewien, że istnieją). Następnie projektant interfejsu API i dokumentator muszą rozróżnić funkcje i właściwości.


Edycja: W szczególności w języku C #, przyjrzałbym się istniejącym klasom, zwłaszcza klasowym.

  • Istnieją BigIntegeri Complexstruktury. Ale są to struktury i mają tylko funkcje statyczne, a wszystkie właściwości są innego rodzaju. Więc nie ma wiele pomocy w projektowaniuHeading klasy.
  • Math.NET Numerics ma Vectorklasę , która ma bardzo mało właściwości i wydaje się być bardzo zbliżona do twojej Heading. Jeśli przejdziesz przez to, będziesz miał funkcję wzajemności, a nie właściwość.

Możesz przyjrzeć się bibliotekom faktycznie używanym przez kod lub istniejącym podobnym klasom. Staraj się być konsekwentny, zwykle jest to najlepszy sposób, jeśli jeden sposób nie jest wyraźnie właściwy, a drugi zły.

hyde
źródło
-1

Rzeczywiście bardzo interesujące pytanie. Nie widzę naruszenia SOLID + DRY + KISS w tym, co proponujesz, ale nadal pachnie źle.

Metoda zwracająca instancję klasy nazywa się konstruktorem , prawda? więc tworzysz nową instancję z metody innej niż konstruktor. To nie koniec świata, ale jako klient nie spodziewałbym się tego. Odbywa się to zwykle w szczególnych okolicznościach: fabrycznej lub, czasem, statycznej metodzie tej samej klasy (przydatne dla singletonów i dla ... niezbyt obiektowych programistów?)

Plus: co się stanie, jeśli wywołasz getReciprocal()zwrócony obiekt? otrzymujesz inną instancję klasy, która może być bardzo dokładną kopią pierwszej! A jeśli się odwołujeszgetReciprocal() TEGO? Przedmioty rozkładają się na kimś?

Ponownie: co jeśli wywołujący potrzebuje wartości wzajemnej (mam na myśli skalar)? getReciprocal()->getValue()? naruszamy Prawo Demeter za niewielkie zyski.

Dr Gianluigi Zane Zanettini
źródło
Chętnie przyjmę kontrargumenty ze strony downvoter, dzięki!
Dr Gianluigi Zane Zanettini
1
Nie głosowałem negatywnie, ale podejrzewam, że powodem może być fakt, że tak naprawdę niewiele wnosi do reszty odpowiedzi, ale mogę się mylić.
53777A,
2
SOLID i KISS są często przeciwieństwami.
whatsisname