Nie deklaruj interfejsów dla obiektów niezmiennych

27

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.

Matthew Watson
źródło
3
+1 świetne pytanie. Wyjaśnij, dlaczego jest dla ciebie tak ważne, że obiekt należy do tej konkretnej niezmiennej klasy, a nie do czegoś, co jedynie implementuje ten sam interfejs. Możliwe, że Twoje problemy są zakorzenione gdzie indziej. Wstrzykiwanie zależności jest niezwykle ważne dla testów jednostkowych, ale ma wiele innych ważnych zalet, które znikają, jeśli wymusisz na swoim kodzie wymaganie określonej niezmiennej klasy.
Steven Doggart
1
Jestem nieco zdezorientowany twoim użyciem terminu niezmienny . Mówiąc wprost, masz na myśli zamkniętą klasę, której nie można odziedziczyć i przesłonić, czy oznacza to, że ujawniane przez nią dane są tylko do odczytu i nie można ich zmienić po utworzeniu obiektu. Innymi słowy, czy chcesz zagwarantować, że obiekt jest określonego typu lub że jego wartość nigdy się nie zmieni. W moim pierwszym komentarzu zakładałem, że chodziło o to pierwsze, ale teraz z twoją edycją brzmi bardziej jak ten drugi. A może interesuje Cię jedno i drugie?
Steven Doggart
3
Jestem zdezorientowany, dlaczego nie zostawić seterów poza interfejsem? Ale z drugiej strony, mam dość oglądania interfejsów dla obiektów, które są tak naprawdę obiektami domenowymi i / lub DTO wymagających fabryk dla tych obiektów ... powoduje dla mnie dysonans poznawczy.
Michael Brown
2
Co o interfejsy dla klas, które są wszystkie niezmienne? Na przykład, Java Ilość klasa pozwala na zdefiniować List<Number>, które może pomieścić Integer, Float, Long, BigDecimal, itd ... Wszystko to są niezmienne same.
1
@MikeBrown Myślę, że to, że interfejs sugeruje niezmienność, nie oznacza, że ​​implementacja go wymusza. Możesz po prostu pozostawić to konwencji (tzn. Udokumentować interfejs, że niezmienność jest wymogiem), ale możesz skończyć z naprawdę paskudnymi problemami, jeśli ktoś naruszy konwencję.
vaughandroid

Odpowiedzi:

17

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.

Steven Doggart
źródło
Kod, który oczekuje konkretnie obsługi DTO lub obiektu o niezmiennej wartości, powinien używać funkcji tego typu, a nie interfejsu, ale to nie znaczy, że nie byłoby przypadków użycia interfejsu implementowanego przez taką klasę, a także przez inne klasy, z metodami, które obiecują zwrócić wartości, które będą ważne przez pewien minimalny czas (np. jeśli interfejs zostanie przekazany do funkcji, metody powinny zwracać prawidłowe wartości, dopóki funkcja nie zwróci). Taki interfejs może być pomocny, jeśli istnieje wiele typów, które zawierają podobne dane i jedno życzenie ...
supercat
... aby mieć możliwość kopiowania danych z jednego do drugiego. Jeśli wszystkie typy, które zawierają określone dane, implementują ten sam interfejs do ich odczytu, wówczas dane te można łatwo skopiować między wszystkimi typami, bez konieczności posiadania wiedzy na temat importowania z każdego innego.
supercat
W tym momencie możesz równie dobrze wyeliminować swoje programy pobierające (i ustawiające, jeśli twoje DTO są zmienne z jakiegoś głupiego powodu) i upublicznić pola. Nigdy nie wprowadzisz tu logiki, prawda?
Kevin,
@Kevin, jak sądzę, ale z nowoczesną wygodą właściwości samochodowych tak łatwo jest nadać im właściwości, dlaczego nie? Podejrzewam, że jeśli wydajność i wydajność są sprawą najwyższej wagi, być może ma to znaczenie. W każdym razie pytanie brzmi: jaki poziom „logiki” dopuszczasz w DTO? Nawet jeśli umieścisz logikę sprawdzania poprawności we właściwościach, nie będzie problemu z jednostkowym testowaniem, o ile nie będzie żadnych zależności, które wymagałyby drwiny. Tak długo, jak DTO nie używa żadnych obiektów biznesowych zależności, można bezpiecznie wbudować w nie małą logikę, ponieważ nadal będą one możliwe do przetestowania.
Steven Doggart
Osobiście wolę, aby były tak czyste, jak to możliwe, ale zawsze zdarza się, że naginanie zasad jest konieczne, więc na wszelki wypadek lepiej zostawić otwarte drzwi.
Steven Doggart
7

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.

Travis Parks
źródło
Zwykle zapisuję interfejsy na wypadek, gdy potrzebuję zastrzyku zależności do testów jednostkowych.
Travis Parks,
5

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.

Telastyn
źródło
2

Kod staje się o wiele prostszy w wielu przypadkach, gdy wiesz, że coś jest niezmienne - czego nie zrobisz, jeśli otrzymasz interfejs.

Nie sądzę, że jest to problem, o który musi się martwić osoba wdrażająca moduł dostępu. Jeśli interface Xma 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.

Jonathan Rich
źródło
1
Czy możesz podać przykład wdrożenia niezmienności jako dekoratora?
vaughandroid
Nie trywialnie, nie sądzę, ale jest to stosunkowo proste ćwiczenie myślowe - jeśli MutableObjectma nmetody zmieniające stan i mmetody zwracające stan, ImmutableDecoratormoż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.
Jonathan Rich
Naprawdę nie lubię przekształcać pewności w czasie kompilacji w możliwość wyjątków w czasie wykonywania ...
Matthew Watson
OK, ale skąd ImmutableDecorator może wiedzieć, czy dana metoda zmienia stan, czy nie?
vaughandroid
W żadnym wypadku nie możesz być pewien, czy klasa implementująca interfejs jest niezmienna w odniesieniu do metod zdefiniowanych przez interfejs. @Baqueta Dekorator musi mieć wiedzę na temat implementacji klasy podstawowej.
Jonathan Rich
0

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.

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>i IQueue<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ć wyniki Pushi Popdo Stack<T>.

Posiadanie Stack<T>narzędzi IStack<T>może być przydatne. Możesz zdefiniować metody, które wymagają stosu, dowolnego stosu, niekoniecznie niezmiennego stosu. Te metody mogą mieć parametr typu IStack<T>. Umożliwia to przekazanie niezmiennych stosów. Idealnie IStack<T>byłoby częścią samej standardowej biblioteki. W .NET nie ma IStack<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.

Hadi Brais
źródło