Czytam C # Design Pattern Essentials . Obecnie czytam o wzorze iteratora.
W pełni rozumiem, jak wdrożyć, ale nie rozumiem znaczenia ani nie widzę przypadku użycia. W książce podano przykład, w którym ktoś musi uzyskać listę obiektów. Mogli to zrobić, ujawniając własność publiczną, taką jak IList<T>
lub Array
.
Książka pisze
Problem polega na tym, że wewnętrzna reprezentacja w obu tych klasach była narażona na zewnętrzne projekty.
Jaka jest wewnętrzna reprezentacja? Fakt, że to jest array
lub IList<T>
? Naprawdę nie rozumiem, dlaczego jest to złe dla konsumenta (programisty nazywającego to) wiedzieć ...
Książka mówi następnie, że ten wzorzec działa, ujawniając jego GetEnumerator
funkcję, dzięki czemu możemy wywoływać GetEnumerator()
i wyświetlać „listę” w ten sposób.
Zakładam, że te wzorce mają miejsce (jak wszystkie) w pewnych sytuacjach, ale nie widzę, gdzie i kiedy.
źródło
F
, który daje mi wiedzę (sposobami)a
,b
ac
. Wszystko jest dobrze i dobrze, może być wiele rzeczy, które są różne, ale tylkoF
dla mnie. Pomyśl o metodzie jako o „ograniczeniach” lub klauzulach umowy, któreF
zobowiązują klasy do działania. Załóżmy, że dodajemyd
jednak, ponieważ go potrzebujemy. Dodaje to dodatkowe ograniczenie, za każdym razem, gdy to robimy, nakładamy coraz więcej naF
s. W końcu (najgorszy przypadek) może być tylko jedna rzecz,F
więc równie dobrze możemy jej nie mieć. IF
ogranicza tak bardzo, że jest tylko jeden sposób, aby to zrobić.Odpowiedzi:
Oprogramowanie to gra obietnic i przywilejów. Nigdy nie jest dobrym pomysłem obiecywać więcej, niż możesz dostarczyć, lub więcej niż potrzebuje Twój współpracownik.
Dotyczy to szczególnie typów. Chodzi o to, aby napisać kolekcję iterowalną, ponieważ jej użytkownik może iterować nad nią - nie więcej, nie mniej. Ujawnienie konkretnego typu
Array
zwykle stwarza wiele dodatkowych obietnic, np. Że możesz posortować kolekcję według wybranej przez ciebie funkcji, nie wspominając o tym, że normalnaArray
prawdopodobnie pozwoli współpracownikowi na zmianę danych, które są w niej przechowywane.Nawet jeśli uważasz, że jest to dobra rzecz („Jeśli moduł renderujący zauważy, że brakuje nowej opcji eksportu, może ją po prostu załatać! Dobrze!”), Ogólnie zmniejsza to spójność podstawy kodu, co utrudnia rozumowanie - a łatwość rozumowania kodu jest najważniejszym celem inżynierii oprogramowania.
Teraz, jeśli twój współpracownik potrzebuje dostępu do wielu rzeczy, aby zagwarantować, że nie umknie żadnej z nich, zaimplementuj
Iterable
interfejs i ujawnisz tylko te metody, które deklaruje ten interfejs. W ten sposób, w przyszłym roku, gdy w twojej standardowej bibliotece pojawi się znacznie lepsza i bardziej wydajna struktura danych, będziesz mógł zmienić kod bazowy i korzystać z niego bez konieczności poprawiania kodu klienta wszędzie . Są jeszcze inne korzyści, że nie obiecujemy więcej niż potrzeba, ale ta sama jest tak duża, że w praktyce nie są potrzebne żadne inne.źródło
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...
- Znakomity. Po prostu o tym nie myślałem. Tak, przywraca „iterację” i tylko iterację!Ukrywanie implementacji jest podstawową zasadą OOP i dobrym pomysłem we wszystkich paradygmatach, ale jest szczególnie ważne dla iteratorów (lub jakkolwiek są one nazywane w tym konkretnym języku) w językach, które obsługują leniwe iteracje.
Problem z ujawnieniem konkretnego typu iterable - a nawet podobnych interfejsów
IList<T>
- nie leży w obiektach, które je ujawniają, ale w metodach, które ich używają. Załóżmy na przykład, że masz funkcję drukowania listyFoo
s:Możesz użyć tej funkcji tylko do drukowania list foo - ale tylko jeśli się zaimplementują
IList<Foo>
Ale co, jeśli chcesz wydrukować każdą pozycję z indeksu parzystego ? Musisz utworzyć nową listę:
Jest to dość długie, ale dzięki LINQ możemy to zrobić na jednej linijce, co w rzeczywistości jest bardziej czytelne:
Czy zauważyłeś
.ToList()
na końcu? Konwertuje to leniwe zapytanie na listę, dzięki czemu możemy go przekazaćPrintFoos
. Wymaga to przydzielenia drugiej listy i dwóch przejść do pozycji (jeden na pierwszej liście, aby utworzyć drugą listę, a drugi na drugiej liście, aby ją wydrukować). A co, jeśli mamy to:Co jeśli
foos
ma tysiące wpisów? Będziemy musieli przejrzeć je wszystkie i przydzielić ogromną listę tylko po to, aby wydrukować 6 z nich!Enter Enumerators - wersja C # wzoru Iterator. Zamiast akceptować listę przez naszą funkcję, przyjmujemy ją
Enumerable
:Teraz
Print6Foos
możemy leniwie powtarzać pierwsze 6 pozycji na liście, a my nie musimy dotykać reszty.Kluczem jest tutaj nieujawnianie wewnętrznej reprezentacji. Po
Print6Foos
przyjęciu listy musieliśmy dać jej listę - coś, co obsługuje losowy dostęp - i dlatego musieliśmy przydzielić listę, ponieważ podpis nie gwarantuje, że będzie się nad nią powtarzał. Ukrywając implementację, możemy łatwo stworzyć bardziej wydajnyEnumerable
obiekt, który obsługuje tylko to, czego faktycznie potrzebuje funkcja.źródło
Ujawnienie wewnętrznej reprezentacji praktycznie nigdy nie jest dobrym pomysłem. Utrudnia to rozumowanie kodu, ale także utrudnia utrzymanie. Wyobraź sobie, że wybrałeś jakąkolwiek wewnętrzną reprezentację - powiedzmy
IList<T>
- i odsłoniłeś to wewnętrzne. Każdy, kto używa Twojego kodu, może uzyskać dostęp do Listy, a kod może polegać na wewnętrznej reprezentacji będącej Listą.Z jakiegokolwiek powodu decydujesz się na zmianę wewnętrznej reprezentacji
IAmWhatever<T>
na później. Zamiast tego po prostu zmieniając elementy wewnętrzne swojej klasy, będziesz musiał zmienić każdy wiersz kodu i metodę, zależnie od typu reprezentacji wewnętrznejIList<T>
. Jest to co najwyżej kłopotliwe i podatne na błędy, gdy jesteś jedynym, który korzysta z klasy, ale może złamać kod innych ludzi korzystających z tej klasy. Jeśli po prostu odsłoniłeś zestaw metod publicznych bez ujawnienia żadnych elementów wewnętrznych, mógłbyś zmienić wewnętrzną reprezentację bez zauważenia jakiegokolwiek wiersza kodu poza klasą, działając tak, jakby nic się nie zmieniło.Dlatego enkapsulacja jest jednym z najważniejszych aspektów każdego nietrywialnego projektu oprogramowania.
źródło
Im mniej mówisz, tym mniej musisz powtarzać.
Im mniej pytasz, tym mniej musisz mówić.
Jeśli kod tylko ujawnia
IEnumerable<T>
, który obsługuje tylkoGetEnumrator()
, można go zastąpić dowolnym innym kodem, który może obsługiwaćIEnumerable<T>
. To dodaje elastyczności.Jeśli Twój kod używa tylko
IEnumerable<T>
, może być obsługiwany przez dowolny kod, który implementujeIEnumerable<T>
. Ponownie istnieje dodatkowa elastyczność.Na przykład wszystkie linie do obiektów zależą tylko od
IEnumerable<T>
. Chociaż przyspiesza ścieżki, niektóre jego konkretne implementacjeIEnumerable<T>
robią to w sposób testowy i użytkowy, który zawsze może wrócić do samego użyciaGetEnumerator()
iIEnumerator<T>
implementacji, która powraca. Daje to znacznie większą elastyczność niż gdyby był zbudowany na tablicach lub listach.źródło
Sformułowanie, które lubię używać mirrorów, komentarz @CaptainMan: „Konsumentowi dobrze jest znać wewnętrzne szczegóły. Źle jest, gdy klient musi znać wewnętrzne szczegóły”.
Jeśli klient musi wpisać
array
lub wpisaćIList<T>
w swoim kodzie, gdy te typy były szczegółami wewnętrznymi, konsument musi je odpowiednio skorygować. Jeśli się mylą, konsument może się nie skompilować, a nawet uzyskać błędne wyniki. Daje to takie same zachowania, jak inne odpowiedzi „zawsze ukryj szczegóły implementacji, ponieważ nie powinieneś ich ujawniać”, ale zaczyna kopać na korzyść braku ujawnienia. Jeśli nigdy nie ujawnisz szczegółów implementacji, Twój klient nigdy nie będzie mógł się związać, używając go!Lubię o tym myśleć w tym świetle, ponieważ otwiera to koncepcję ukrywania implementacji w odcieniach znaczenia, a nie drakońskiego „zawsze ukryj szczegóły implementacji”. Istnieje wiele sytuacji, w których ujawnianie implementacji jest całkowicie ważne. Rozważmy przypadek sterowników urządzeń w czasie rzeczywistym, w których zdolność pomijania przeszłych warstw abstrakcji i umieszczania bajtów w odpowiednich miejscach może być różnicą między ustalaniem czasu lub nie. Lub rozważ środowisko programistyczne, w którym nie masz czasu na tworzenie „dobrych interfejsów API” i musisz natychmiast z nich korzystać. Często ujawnianie bazowych interfejsów API w takich środowiskach może być różnicą między ustaleniem terminu, czy nie.
Jednak gdy zostawiasz te specjalne przypadki za sobą i patrzysz na bardziej ogólne przypadki, zaczyna być coraz ważniejsze ukrywanie implementacji. Ujawnianie szczegółów implementacji jest jak lina. Użyte właściwie, możesz robić z nim różne rzeczy, takie jak skalowanie wysokich gór. Używana nieprawidłowo i może wiązać cię w pętlę. Im mniej wiesz o tym, jak będzie używany Twój produkt, tym bardziej musisz być ostrożny.
Studium przypadku, które widzę od czasu do czasu, to takie, w którym programista tworzy interfejs API, który nie bardzo ostrożnie ukrywa szczegóły implementacji. Drugi programista, korzystający z tej biblioteki, stwierdza, że API brakuje niektórych kluczowych funkcji, które chcieliby. Zamiast pisać prośbę o funkcję (która może potrwać kilka miesięcy), zamiast tego decydują się nadużyć dostępu do ukrytych szczegółów implementacji (co zajmuje kilka sekund). Samo w sobie nie jest to złe, ale często deweloper API nigdy nie planował, że będzie częścią API. Później, kiedy udoskonalą interfejs API i zmienią ten podstawowy szczegół, okaże się, że są przywiązani do starej implementacji, ponieważ ktoś użył go jako części interfejsu API. Najgorsza wersja tego jest taka, że ktoś użył twojego API, aby zapewnić funkcję zorientowaną na klienta, a teraz Twoja firma wypłata jest powiązana z tym błędnym API! Po prostu ujawniając szczegóły implementacji, gdy nie wiedziałeś wystarczająco dużo o swoich klientach, okazało się, że wystarczy liny, aby zawiązać pętlę wokół twojego API!
źródło
Niekoniecznie wiesz, jaka jest wewnętrzna reprezentacja twoich danych - lub może stać się w przyszłości.
Załóżmy, że masz źródło danych, które reprezentujesz jako tablicę, możesz iterować je za pomocą zwykłej pętli for.
Teraz powiedz, że chcesz refaktoryzować. Twój szef poprosił, aby źródłem danych był obiekt bazy danych. Ponadto chciałby mieć opcję pobierania danych z interfejsu API, mapy skrótów lub powiązanej listy, obracającego się bębna magnetycznego lub mikrofonu lub zliczania ilości wiórów jak w Mongolii Zewnętrznej w czasie rzeczywistym.
Cóż, jeśli masz tylko jedną pętlę dla, możesz łatwo zmodyfikować kod, aby uzyskać dostęp do obiektu danych w sposób, w jaki oczekuje on dostępu.
Jeśli masz 1000 klas próbujących uzyskać dostęp do obiektu danych, teraz masz duży problem z refaktoryzacją.
Iterator to standardowy sposób dostępu do źródła danych opartego na liście. To wspólny interfejs. Użycie go sprawi, że Twój kod źródłowy danych będzie agnostyczny.
źródło