„Użyj mapy zamiast klasy do przedstawienia danych” - Rich Hickey

19

W tym filmie Richa Hickeya , twórcy Clojure, zaleca się używanie mapy do reprezentowania danych zamiast używania klasy do ich reprezentowania, jak ma to miejsce w Javie. Nie rozumiem, jak może być lepiej, skoro użytkownik interfejsu API może wiedzieć, jakie są klucze wejściowe, jeśli są po prostu reprezentowane jako mapy.

Przykład :

PersonAPI {
    Person addPerson(Person obj);
    Map<String, Object> addPerson(Map<String, Object> personMap);
}

W drugiej funkcji, w jaki sposób użytkownik API może wiedzieć, jakie są dane wejściowe do stworzenia osoby?

Emil
źródło
Chciałbym to również wiedzieć i uważam, że przykładowe pytanie nie do końca na nie odpowiada.
sydan
Ja wiem, widziałem tę dyskusję przed gdzieś na SE. Wierzę, że było to w kontekście JavaScript, ale argumenty były takie same. Nie mogę tego znaleźć.
Sebastian Redl,
2
Ponieważ Clojure jest Lispem, powinieneś robić rzeczy odpowiednie dla Lisp. kiedy używasz Javy, napisz ... dobrze Java.
AK_

Odpowiedzi:

12

Exagg'itive Podsumowanie (TM)

Dostajesz kilka rzeczy.

  • Dziedziczenie prototypowe i klonowanie
  • Dynamiczne dodawanie nowych właściwości
  • Współistnienie obiektów różnych wersji (poziomów specyfikacji) tej samej klasy.
    • Obiekty należące do nowszych wersji (poziomy specyfikacji) będą miały dodatkowe „opcjonalne” właściwości.
  • Spojrzenie na nieruchomości, stare i nowe
  • Introspekcja zasad walidacji (omówione poniżej)

Jest jedna poważna wada.

  • Kompilator nie sprawdza dla Ciebie literówek.
  • Narzędzia do automatycznego refaktoryzacji nie zmienią dla ciebie nazw kluczy własności - chyba że zapłacisz za fantazyjne.

Chodzi o to, że można uzyskać introspekcję, używając, um, introspekcji. Tak zwykle się dzieje:

  • Włącz odbicie.
  • Dodaj dużą bibliotekę introspekcji do swojego projektu.
  • Oznacz różne metody i właściwości obiektów atrybutami lub adnotacjami.
  • Niech biblioteka introspekcji wykona magię.

Innymi słowy, jeśli nigdy nie potrzebujesz interfejsu z FP, nie musisz brać rady Richa Hickeya.

Na koniec, ale nie najmniej (ani najładniejszy), chociaż użycie Stringjako klucza właściwości ma najprostszy sens, nie musisz używać Strings. Wiele starszych systemów, w tym Android ™, intensywnie wykorzystuje identyfikatory liczb całkowitych w całej strukturze w odniesieniu do klas, właściwości, zasobów itp.

Android jest znakiem towarowym firmy Google Inc.


Możesz także uszczęśliwić oba światy.

W świecie Java zaimplementuj metody pobierające i ustawiające jak zwykle.

Dla świata FP zaimplementuj

  • Object getPropertyByName(String name)
  • void setPropertyByName(String name, Object value) throws IllegalPropertyChangeException
  • List<String> getPropertyNames()
  • Class<?> getPropertyValueClass(String name)

Wewnątrz tych funkcji, tak, brzydki kod, ale istnieją wtyczki IDE, które wypełnią to za Ciebie, używając ... uh, inteligentnej wtyczki, która odczytuje twój kod.

Strona Java będzie tak samo wydajna jak zwykle. Nigdy nie użyją tej brzydkiej części kodu. Możesz nawet chcieć ukryć to przed Javadoc.

Strona FP na świecie może napisać dowolny kod „leet”, jaki chce, i zazwyczaj nie krzyczy na ciebie, że kod jest powolny.


Ogólnie rzecz biorąc, używanie mapy (torby właściwości) zamiast obiektu jest powszechne w programowaniu. Nie jest on unikalny dla programowania funkcjonalnego ani żadnego konkretnego rodzaju języków. To może nie być podejście idiomatyczne dla danego języka, ale są sytuacje, które tego wymagają.

W szczególności serializacja / deserializacja często wymaga podobnej techniki.

Kilka ogólnych przemyśleń na temat „mapy jako obiektu”.

  1. Nadal musisz zapewnić funkcję sprawdzania poprawności takiej „mapy jako obiektu”. Różnica polega na tym, że „mapa jako obiekt” pozwala na bardziej elastyczne (mniej restrykcyjne) kryteria sprawdzania poprawności.
  2. Możesz łatwo dodawać pola dodatkowe do „mapy jako obiektu”.
  3. Aby podać specyfikację minimalnego wymagania prawidłowego obiektu, musisz:
    • Wymień „minimalnie wymagany” zestaw kluczy oczekiwanych na mapie
    • Dla każdego klucza, którego wartość wymaga walidacji, podaj funkcję walidacji wartości
    • Jeśli istnieją reguły sprawdzania poprawności, które muszą sprawdzać wiele wartości klucza, również to podaj.
    • Jaka jest korzyść? Podanie specyfikacji w ten sposób jest introspektywne: możesz napisać program, który będzie sprawdzał minimalnie wymagany zestaw kluczy i uzyskał funkcję sprawdzania poprawności dla każdego klucza.
    • W OOP wszystkie są zwinięte w czarną skrzynkę, w imię „enkapsulacji”. Zamiast logiki sprawdzania poprawności odczytywanej maszynowo, wywołujący może czytać tylko „dokumentację API” czytelną dla człowieka (jeśli na szczęście istnieje).
rwong
źródło
commonplacewydaje mi się trochę silny. Mam na myśli, że jest używany tak, jak to opisujesz, ale jest także jedną z tych notorycznie niestabilnych / kruchych rzeczy (takich jak tablice bajtów lub odsłonięte wskaźniki), które biblioteki starają się jak najdalej ukryć.
Telastyn
@Telastyn Ta „brzydka głowa tysiąca węży” zwykle występuje na granicy komunikacji między dwoma systemami, gdzie z jakiegoś powodu kanał komunikacji lub międzyprocesowy nie pozwala na teleportację obiektów w stanie nienaruszonym. Sądzę, że nowe techniki, takie jak bufory protokołów, prawie wyeliminowały ten archaiczny przypadek użycia mapy jako obiektu. Mogą być jeszcze inne ważne przypadki użycia, ale nie mam o tym wiedzy.
rwong
2
Jeśli chodzi o fatalne wady, zgódź się. Ale jeśli nazwy kluczy właściwości „łatwa do błędnego wpisania” i „trudna do refaktoryzacji” są przechowywane, w miarę możliwości, w stałych lub wyliczeniach , problem ten zniknie. Oczywiście, ogranicza to niektóre rozszerzenia :-(.
user949300
Jeśli „jedna fatalna wada” jest naprawdę śmiertelna, dlaczego niektórzy ludzie są w stanie ją skutecznie wykorzystać. Ponadto klasy i pisanie statyczne są ortogonalne - możesz definiować klasy w Clojure, nawet jeśli są one dynamicznie wpisywane.
Nathan Davis,
@NathanDavis (1) Przyznaję, że moja odpowiedź została napisana z perspektywy pisania statycznego (C #) i napisałem tę odpowiedź, ponieważ podzielam ten sam punkt widzenia pytającego. Przyznaję, że brakuje mi punktu widzenia skoncentrowanego na FP. (2) Witamy w SE.SE, a ponieważ jesteś szanowaną postacią w Clojure, prosimy o poświęcenie czasu na napisanie własnej odpowiedzi, jeśli istniejące nie są zadowalające. Downvotes odejmują reputację, a nowe odpowiedzi przyciągają upvotes, które szybko sumują reputację. (3) Widzę, jak przydatne mogą być „niekompletne obiekty” - możesz wyszukać 2 właściwości dla danego obiektu (nazwy, awatara) i pominąć resztę.
rwong
9

To doskonała rozmowa kogoś, kto naprawdę wie, o czym mówi. Polecam czytelnikom obejrzenie całości. To tylko 36 minut.

Jednym z jego głównych punktów jest to, że prostota otwiera możliwości późniejszych zmian. Wybór klasy, która ma reprezentować, Personzapewnia natychmiastową korzyść z utworzenia statystycznie weryfikowalnego interfejsu API, jak wskazano, ale wiąże się to z kosztem ograniczenia możliwości lub zwiększenia kosztów zmian i ponownego wykorzystania w późniejszym terminie.

Chodzi mu o to, że korzystanie z klasy może być rozsądnym wyborem, ale powinien być świadomym wyborem z pełną świadomością jego kosztów, a programiści zwykle bardzo słabo zauważają te koszty, nie mówiąc już o ich uwzględnieniu. Ten wybór powinien zostać ponownie oceniony w miarę wzrostu wymagań.

Oto niektóre zmiany kodu (z których jedna lub dwie zostały wspomniane w dyskusji), które są potencjalnie łatwiejsze przy użyciu listy map niż przy użyciu listy Personobiektów:

  • Wysyłanie osoby na serwer REST. (Funkcja utworzona w celu umieszczenia Mapprymitywów w formacie, który można przesłać, nadaje się do wielokrotnego użytku i może nawet być udostępniana w bibliotece. PersonObiekt prawdopodobnie potrzebuje niestandardowego kodu do wykonania tego samego zadania).
  • Automatycznie konstruuj listę osób z kwerendy relacyjnej bazy danych. (Ponownie, jedna ogólna funkcja wielokrotnego użytku).
  • Automatycznie wygeneruj formularz, aby wyświetlić i edytować osobę.
  • Używaj typowych funkcji do pracy z danymi osób, które są wysoce niejednorodne, np. Uczeń kontra pracownik.
  • Uzyskaj listę wszystkich osób zamieszkałych w określonym kodzie pocztowym.
  • Użyj tego kodu ponownie, aby uzyskać listę wszystkich firm w określonym kodzie pocztowym.
  • Dodaj pole specyficzne dla klienta do osoby bez wpływu na innych klientów.

Cały czas rozwiązujemy tego rodzaju problemy, dysponujemy wzorcami i narzędziami, ale rzadko zastanawiamy się, czy wybór prostszej, bardziej elastycznej reprezentacji danych na początku ułatwiłby nam pracę.

Karl Bielefeldt
źródło
Czy jest na to jakaś nazwa? Powiedzmy, mapowanie właściwości obiektu lub mapowanie atrybutu obiektu (wzdłuż tej samej linii co ORM)?
rwong
4
Choosing a class to represent a Person provides the immediate benefit of creating a statically-verifiable API... but that comes with the cost of limiting opportunities or increasing costs for change and reuse later on.Źle i niezwykle nieuczciwie. To zwiększa swoją szansę na zmianę w przyszłości, ponieważ po dokonaniu zmiany na rozerwanie, kompilator automatycznie znaleźć i wskazać dla Ciebie każde miejsce, które musi zostać zaktualizowana, aby przynieść cały codebase do prędkości. Jest w kodzie dynamicznym, gdzie nie możesz tego zrobić, że naprawdę łączysz się z poprzednimi wyborami!
Mason Wheeler,
4
@MasonWheeler: Naprawdę mówisz, że cenisz bezpieczeństwo typu kompilacji w porównaniu z bardziej dynamicznymi (i bardziej luźnymi) strukturami danych.
Robert Harvey
1
Polimorfizm nie jest pojęciem ograniczonym do OOP. W przypadku map możesz mieć polimorfizm inkluzywny (jeśli elementy są podtypami jakiegoś typu, który mapa może obsłużyć) lub polimorfizm ad-hoc (jeśli elementy są oznaczone związkami). To są elementy wewnętrzne. Operacje, które można wykonać na mapie, mogą być również polimorficzne. Polimorfizm parametryczny, gdy używamy funkcji wyższego rzędu na elementach lub ad-hoc podczas wysyłania. Kapsułkowanie można osiągnąć za pomocą przestrzeni nazw lub innych form zarządzania widocznością. Zasadniczo izolacja obiektów nie równa się przypisywaniu operacji do typów danych.
siefca
1
@GillBates, dlaczego tak mówisz? Po prostu tracisz możliwość umieszczenia tych wirtualnych metod „wewnątrz mapy” - ale dokładnie o tym mówi Rich Hickey, „ActiveObjects” są naprawdę anty-wzorcem. Należy traktować dane jako takie, jakie są (dane), a nie przeplatać je z zachowaniem. Rozdzielając obawy, można osiągnąć ogromne korzyści w zakresie prostoty.
Wergiliusz
4
  • Jeśli dane zachowują się słabo lub nie zachowują się przy elastycznych treściach, które mogą ulec zmianie, skorzystaj z mapy. IMO, typowy „javabean” lub „obiekt danych”, który składa się z modelu domeny anemicznej z N polami, N ustawiającymi i N pobierającymi, jest stratą czasu. Nie próbuj zaimponować innym swoją chwalebną strukturą, zawijając ją w fantazyjną klasę smancy. Bądź szczery, wyraź swoje intencje i użyj mapy. (Lub, jeśli ma to jakiś sens dla Twojej domeny, obiektu JSON lub XML)

  • Jeśli dane mają znaczące rzeczywiste zachowanie, czyli metody ( Tell, Don't Ask ), użyj klasy. I poklep się po plecach za używanie prawdziwego programowania obiektowego :-).

  • Jeśli dane mają wiele zasadniczych zachowań związanych z weryfikacją i wymagane pola, użyj klasy.

  • Jeśli dane mają umiarkowaną liczbę operacji sprawdzania poprawności, jest to granica.

  • Jeśli dane wyzwalają zmiany właściwości nieruchomości, jest to w rzeczywistości łatwiejsze i znacznie mniej uciążliwe dzięki Mapie. Po prostu napisz małą podklasę.

  • Jedną z głównych wad korzystania z mapy jest to, że użytkownik musi rzucić wartości na ciągi, ints, foos itp. Jeśli jest to bardzo denerwujące i podatne na błędy, rozważ klasę. Lub rozważ klasę pomocnika, która otacza mapę odpowiednimi pobierającymi.

użytkownik949300
źródło
1
Właściwie to, co twierdzi Rich Hickey, to fakt, że jeśli dane mają znaczące rzeczywiste zachowanie ... prawdopodobnie źle robisz całe „projektowanie”. Dane to „informacje”. Informacje w prawdziwym świecie NIE są „miejscem przechowywania danych”. Informacje nie obejmują „operacji kontrolujących zmiany informacji”. Nie przekazujemy informacji, mówiąc ludziom, gdzie są przechowywane. Zorientowane obiektowo metafory są CZASEM odpowiednim modelem świata ... ale najczęściej nie są. Tak mówi - „pomyśl o problemie z ypur”. Nie wszystko jest przedmiotem - niewiele jest rzeczy.
Wergiliusz
0

Interfejs API dla mapma dwa poziomy.

  1. API dla map.
  2. Konwencje aplikacji.

Interfejs API można opisać na mapie zgodnie z konwencją. Na przykład para :api api-validatemoże być umieszczona na mapie lub :api-foo validate-foomoże być konwencją. Mapa może nawet przechowywać api api-documentation-link.

Korzystanie z konwencji pozwala programiście stworzyć język specyficzny dla domeny, który standaryzuje dostęp do „typów” zaimplementowanych jako mapy. Użycie (keys map)pozwala na określenie właściwości w czasie wykonywania.

Nie ma nic magicznego w mapach i nie ma nic magicznego w obiektach. To wszystko jest wysyłka.

Ben Rudgers
źródło