Nie deklaruj interfejsów dla obiektów niezmiennych
[EDYCJA] Gdy przedmiotowe obiekty reprezentują obiekty przesyłania danych (DTO) lub zwykłe stare dane (POD)
Czy to rozsądna wskazówka?
Do tej pory często tworzyłem interfejsy dla zapieczętowanych klas, które są niezmienne (danych nie można zmienić). Sam starałem się nie używać interfejsu w żadnym miejscu, w którym zależy mi na niezmienności.
Niestety interfejs zaczyna przenikać do kodu (martwię się nie tylko o mój kod). Skończyło się na tym, że przeszedłeś przez interfejs, a potem chcesz przekazać go do jakiegoś kodu, który naprawdę chce założyć, że przekazywana do niego rzecz jest niezmienna.
Z powodu tego problemu rozważam nigdy nie deklarowanie interfejsów dla niezmiennych obiektów.
Może to mieć konsekwencje w odniesieniu do testów jednostkowych, ale poza tym, czy wydaje się to rozsądną wskazówką?
Czy może jest inny wzór, którego powinienem użyć, aby uniknąć problemu z interfejsem rozprzestrzeniania, który widzę?
(Używam tych niezmiennych obiektów z kilku powodów: Głównie dla bezpieczeństwa wątków, ponieważ piszę dużo wielowątkowego kodu; ale także dlatego, że oznacza to, że mogę uniknąć tworzenia obronnych kopii obiektów przekazywanych do metod. Kod staje się o wiele prostszy w w wielu przypadkach, gdy wiesz, że coś jest niezmienne - czego nie masz, jeśli otrzymałeś interfejs. W rzeczywistości często nie możesz nawet wykonać defensywnej kopii obiektu, do którego odwołuje się interfejs, jeśli nie zapewnia klonowanie lub jakikolwiek sposób szeregowania go ...)
[EDYTOWAĆ]
Aby podać znacznie więcej kontekstu z moich powodów, dla których chcę, aby obiekty były niezmienne, zobacz ten post na blogu autorstwa Erica Lipperta:
http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/
Powinienem również zauważyć, że pracuję tutaj z niektórymi koncepcjami niższego poziomu, takimi jak elementy, które są manipulowane / przekazywane w wielowątkowych kolejkach zadań. Są to zasadniczo DTO.
Również Joshua Bloch zaleca korzystanie z niezmiennych obiektów w swojej książce Effective Java .
Zagryźć
Dzięki za opinie. Postanowiłem pójść dalej i skorzystać z tych wskazówek dla DTO i ich podobnych. Jak dotąd działa dobrze, ale minął tylko tydzień ... Wygląda dobrze.
Jest kilka innych kwestii związanych z tym, o które chcę zapytać; zwłaszcza coś, co nazywam „głęboką lub płytką niezmiennością” (nazewnictwo, które ukradłem z klonowania głębokiego i płytkiego) - ale to pytanie jest na inny czas.
źródło
List<Number>
, które może pomieścićInteger
,Float
,Long
,BigDecimal
, itd ... Wszystko to są niezmienne same.Odpowiedzi:
Moim zdaniem twoja reguła jest dobra (a przynajmniej nie jest zła), ale tylko z powodu opisywanej sytuacji. Nie powiedziałbym, że zgadzam się z tym we wszystkich sytuacjach, więc z punktu widzenia mojego wewnętrznego pedanta musiałbym powiedzieć, że twoja zasada jest technicznie zbyt szeroka.
Zazwyczaj nie definiujesz niezmiennych obiektów, chyba że są one zasadniczo używane jako obiekty do przesyłania danych (DTO), co oznacza, że zawierają właściwości danych, ale bardzo mało logiki i żadnych zależności. W takim przypadku, jak się wydaje, jest tutaj, powiedziałbym, że możesz bezpiecznie używać konkretnych typów bezpośrednio zamiast interfejsów.
Jestem pewien, że będzie kilku purystów testujących jednostki, którzy się z tym nie zgodzą, ale moim zdaniem klasy DTO można bezpiecznie wykluczyć z wymagań testów jednostkowych i wstrzykiwania zależności. Nie ma potrzeby używania fabryki do tworzenia DTO, ponieważ nie ma ona żadnych zależności. Jeśli wszystko tworzy DTO bezpośrednio w razie potrzeby, to tak naprawdę nie ma sposobu na wstrzyknięcie innego typu, więc nie ma potrzeby interfejsu. A ponieważ nie zawierają logiki, nie ma nic do testowania jednostkowego. Nawet jeśli zawierają one logikę, o ile nie mają zależności, testowanie logiki jednostkowej w razie potrzeby powinno być trywialne.
W związku z tym uważam, że wprowadzenie zasady, że wszystkie klasy DTO nie będą implementować interfejsu, choć potencjalnie będą niepotrzebne, nie zaszkodzi projektowi oprogramowania. Ponieważ masz ten wymóg, że dane muszą być niezmienne i nie możesz tego egzekwować za pośrednictwem interfejsu, powiedziałbym, że ustanowienie tej reguły jako standardu kodowania jest całkowicie uzasadnione.
Większym problemem jest jednak konieczność ścisłego wymuszenia czystej warstwy DTO. Tak długo, jak twoje niezmienne interfejsy bez interfejsu będą istniały tylko w warstwie DTO, a twoja warstwa DTO pozostanie wolna od logiki i zależności, będziesz bezpieczny. Jeśli jednak zaczniesz mieszać warstwy i masz klasy bez interfejsu, które podwajają się jak klasy warstw biznesowych, myślę, że zaczniesz mieć duże problemy.
źródło
Kiedyś robiłem wielkie zamieszanie, czyniąc mój kod niewrażliwym na niewłaściwe użycie. Stworzyłem interfejsy tylko do odczytu do ukrywania mutujących członków, dodałem wiele ograniczeń do moich ogólnych podpisów itp. Itp. Okazało się, że przez większość czasu podejmowałem decyzje projektowe, ponieważ nie ufałem moim wyobrażonym współpracownikom. „Być może kiedyś zatrudnią nowego faceta z klasy podstawowej i nie będzie wiedział, że klasa XYZ nie może zaktualizować DTO ABC. Och nie!” Innym razem skupiałem się na niewłaściwym problemie - ignorując oczywiste rozwiązanie - nie widząc lasu przez drzewa.
Nigdy nie tworzę interfejsów dla moich DTO. Pracuję przy założeniu, że osoby dotykające mojego kodu (przede wszystkim ja) wiedzą, co jest dozwolone i co ma sens. Jeśli wciąż popełniam ten sam głupi błąd, zwykle nie próbuję utwardzać interfejsów. Teraz spędzam większość czasu próbując zrozumieć, dlaczego ciągle popełniam ten sam błąd. Zwykle dzieje się tak dlatego, że zbytnio analizuję coś lub brakuje mi kluczowej koncepcji. Mój kod jest znacznie łatwiejszy w obsłudze, odkąd przestałem być paranoikiem. Skończyło się też na mniejszej liczbie „frameworków”, które wymagały wiedzy poufnych do pracy w systemie.
Moim wnioskiem było znalezienie najprostszej rzeczy, która działa. Dodatkowa złożoność tworzenia bezpiecznych interfejsów po prostu marnuje czas programowania i komplikuje prosty kod. Martw się o takie rzeczy, gdy masz 10 000 programistów korzystających z bibliotek. Uwierz mi, uratuje cię to od niepotrzebnego napięcia.
źródło
Wydaje się, że to dobra wskazówka, ale z dziwnych powodów. Miałem wiele miejsc, w których interfejs (lub abstrakcyjna klasa bazowa) zapewnia jednolity dostęp do szeregu niezmiennych obiektów. Strategie zwykle upadają tutaj. Przedmioty państwowe mają tendencję do spadania tutaj. Nie sądzę, aby kształtowanie interfejsu wydawało się niezmienne i dokumentowanie go jako takiego w interfejsie API jest zbyt nierozsądne.
To powiedziawszy, ludzie często przeciążają obiekty Plain Old Data (dalej POD), a nawet proste (często niezmienne) struktury. Jeśli twój kod nie ma rozsądnych alternatyw dla jakiejś podstawowej struktury, nie potrzebuje interfejsu. Nie, testowanie jednostkowe nie jest wystarczającym powodem do zmiany projektu (udawanie dostępu do bazy danych nie jest powodem, dla którego udostępniasz interfejs, jest to elastyczność dla przyszłych zmian) - to nie koniec świata, jeśli twoje testy używaj tej podstawowej podstawowej struktury taką, jaka jest.
źródło
Nie sądzę, że jest to problem, o który musi się martwić osoba wdrażająca moduł dostępu. Jeśli
interface X
ma to być niezmienne, to czy to nie implementator interfejsu jest odpowiedzialny za zapewnienie, że implementują interfejs w niezmienny sposób?Jednak moim zdaniem nie ma czegoś takiego jak niezmienny interfejs - określona przez interfejs umowa kodu dotyczy tylko odsłoniętych metod obiektu, a nic nie dotyczy elementów wewnętrznych obiektu.
O wiele bardziej powszechne jest postrzeganie niezmienności zaimplementowanej raczej jako dekorator niż interfejs, ale wykonalność tego rozwiązania naprawdę zależy od struktury obiektu i złożoności implementacji.
źródło
MutableObject
man
metody zmieniające stan im
metody zwracające stan,ImmutableDecorator
może nadal ujawniać metody, które zwracają stan (m
) i, w zależności od środowiska, twierdzić lub zgłasza wyjątek, gdy wywoływana jest jedna z metod zmiennych.W języku C # metoda, która oczekuje obiektu typu „niezmiennego”, nie powinna mieć parametru typu interfejsu, ponieważ interfejsy C # nie mogą określać umowy niezmienności. Dlatego wytyczna, którą sam proponujesz, nie ma znaczenia w języku C #, ponieważ nie możesz tego zrobić w pierwszej kolejności (i jest mało prawdopodobne, aby przyszłe wersje języków pozwoliły ci to zrobić).
Twoje pytanie wynika z subtelnego niezrozumienia artykułów Erica Lipperta na temat niezmienności. Eric nie zdefiniował interfejsów
IStack<T>
iIQueue<T>
nie określił kontraktów na niezmienne stosy i kolejki. Oni nie. Zdefiniował je dla wygody. Interfejsy te pozwoliły mu zdefiniować różne typy pustych stosów i kolejek. Możemy zaproponować inny projekt i implementację niezmiennego stosu przy użyciu jednego typu bez wymagania interfejsu lub osobnego typu do reprezentowania pustego stosu, ale wynikowy kod nie będzie wyglądał tak czysto i byłby nieco mniej wydajny.Pozostańmy teraz przy projekcie Erica. Metoda wymagająca niezmiennego stosu musi mieć parametr typu
Stack<T>
zamiast ogólnego interfejsu,IStack<T>
który reprezentuje abstrakcyjny typ danych stosu w ogólnym znaczeniu. Nie jest oczywiste, jak to zrobić, używając niezmiennego stosu Erica, a on nie omawiał tego w swoich artykułach, ale jest to możliwe. Problem polega na rodzaju pustego stosu. Możesz rozwiązać ten problem, upewniając się, że nigdy nie otrzymasz pustego stosu. Można to zapewnić, naciskając wartość fikcyjną jako pierwszą wartość na stosie i nigdy nie usuwając jej. W ten sposób możesz bezpiecznie przesyłać wynikiPush
iPop
doStack<T>
.Posiadanie
Stack<T>
narzędziIStack<T>
może być przydatne. Możesz zdefiniować metody, które wymagają stosu, dowolnego stosu, niekoniecznie niezmiennego stosu. Te metody mogą mieć parametr typuIStack<T>
. Umożliwia to przekazanie niezmiennych stosów. IdealnieIStack<T>
byłoby częścią samej standardowej biblioteki. W .NET nie maIStack<T>
, ale istnieją inne standardowe interfejsy, które niezmienny stos może zaimplementować, dzięki czemu ten typ jest bardziej użyteczny.Alternatywne projektowanie i implementacja niezmiennego stosu, o którym wspomniałem wcześniej, wykorzystuje interfejs o nazwie
IImmutableStack<T>
. Oczywiście, wstawienie „Immutable” w nazwie interfejsu nie powoduje, że każdy typ, który go implementuje, jest niezmienny. Jednak w tym interfejsie umowa niezmienności jest po prostu ustna. Dobry programista powinien to uszanować.Jeśli opracowujesz małą, wewnętrzną bibliotekę, możesz zgodzić się ze wszystkimi członkami zespołu, aby dotrzymać tego kontraktu i możesz użyć
IImmutableStack<T>
jako rodzaju parametrów. W przeciwnym razie nie należy używać typu interfejsu.Chciałbym dodać, ponieważ otagowałeś pytanie C #, że w specyfikacji C # nie ma czegoś takiego jak DTO i POD. Dlatego odrzucenie ich lub precyzyjne zdefiniowanie poprawia pytanie.
źródło