Jakie są wady mapowania integralnych identyfikatorów na wyliczenia?

16

Zastanawiałem się nad stworzeniem niestandardowych typów identyfikatorów takich jak ten:

public enum CustomerId : int { /* intentionally empty */ }
public enum OrderId : int { }
public enum ProductId : int { }

Moją podstawową motywacją do tego jest zapobieganie rodzajowi błędu, w którym przypadkowo przekazujesz orderItemId do funkcji, która oczekiwała orderItemDetailId.

Wygląda na to, że wyliczenia działają bezproblemowo ze wszystkim, czego chciałbym użyć w typowej aplikacji internetowej .NET:

  • Routing MVC działa dobrze
  • Serializacja JSON działa dobrze
  • Każda ORM, o której mogę pomyśleć, działa dobrze

Zastanawiam się teraz: „dlaczego miałbym tego nie robić?” To jedyne wady, jakie mogę wymyślić:

  • Może to mylić innych programistów
  • Wprowadza niespójność w systemie, jeśli masz jakieś integralne identyfikatory.
  • Może to wymagać dodatkowego rzucania (CustomerId)42. Ale nie sądzę, że będzie to problem, ponieważ routing ORM i MVC zazwyczaj przekazuje ci bezpośrednio wartości typu wyliczeniowego.

Więc moje pytanie brzmi: czego mi brakuje? To chyba zły pomysł, ale dlaczego?

default.kramer
źródło
3
Nazywa się to „microtyping”, można google wokół (np to jest dla Javy, szczegóły są różne, ale motywacja sama)
QBD
Napisałem dwie częściowe odpowiedzi, a następnie je usunąłem, ponieważ zdałem sobie sprawę, że źle myślę. Zgadzam się z Tobą, że „To prawdopodobnie zły pomysł, ale dlaczego?”
user2023861

Odpowiedzi:

7

To świetny pomysł, aby utworzyć typ dla każdego rodzaju identyfikatora, głównie z podanych powodów. Nie zrobiłbym tego, implementując typ jako wyliczenie, ponieważ uczynienie go odpowiednią strukturą lub klasą daje więcej opcji:

  • Możesz dodać niejawną konwersję do int. Kiedy się nad tym zastanowić, to ten drugi kierunek, int -> CustomerId, jest problematyczny i zasługuje na wyraźne potwierdzenie od konsumenta.
  • Możesz dodać reguły biznesowe określające, które identyfikatory są dopuszczalne, a które nie. Nie chodzi tylko o słabe sprawdzenie, czy int jest dodatni: czasami lista wszystkich możliwych identyfikatorów jest przechowywana w bazie danych, ale zmienia się tylko podczas wdrażania. Następnie możesz łatwo sprawdzić, czy podany identyfikator istnieje w pamięci podręcznej wyniku prawidłowego zapytania. W ten sposób możesz zagwarantować, że aplikacja szybko zawiedzie w przypadku nieprawidłowych danych.
  • Metody identyfikatora mogą pomóc w przeprowadzaniu kosztownych kontroli istnienia bazy danych lub uzyskaniu odpowiedniego obiektu encji / domeny.

O implementacji tego możesz przeczytać w artykule Functional C #: Primitive obsesja .

Lukáš Lánský
źródło
1
powiedziałbym, że prawie na pewno nie chcesz zawijać każdej liczby int w klasie, ale w strukturze
jk.
Dobra uwaga, nieznacznie zaktualizowałem odpowiedź.
Lukáš Lánský
3

Jest to sprytny hack, ale nadal hack, który nadużywa funkcji czegoś, co nie jest zamierzonym celem. Hacki mają koszt utrzymania w utrzymaniu, więc nie wystarczy, że nie możesz znaleźć żadnych innych wad, musi także mieć znaczące korzyści w porównaniu z bardziej idiomatycznym sposobem rozwiązania tego samego problemu.

Idiomatycznym sposobem byłoby utworzenie typu opakowania lub struktury z jednym polem. Zapewni to bezpieczeństwo tego samego typu w bardziej wyraźny i idiomatyczny sposób. Chociaż twoja propozycja jest wprawdzie sprytna, nie widzę żadnych znaczących korzyści z używania typów opakowań.

JacquesB
źródło
1
Główną zaletą wyliczenia jest to, że „po prostu działa” tak, jak chcesz z MVC, serializacją, ORM itp. W przeszłości tworzyłem niestandardowe typy (struktury i klasy) i kończysz pisać więcej kodu niż ty powinno być konieczne, aby te frameworki / biblioteki działały z niestandardowymi typami.
default.kramer
Serializacja powinna działać w obu przypadkach w sposób przejrzysty, ale np. ORM musisz skonfigurować jakąś jawną konwersję. Jest to jednak cena mocno napisanego języka.
JacquesB
2

Z mojego POV pomysł jest dobry. Intencja nadania różnych typów niepowiązanym klasom identyfikatorów, a w inny sposób wyrażenia semantyki za pomocą typów i umożliwienia kompilatorowi sprawdzenia, jest dobra.

Nie wiem, jak to działa pod kątem wydajności .NET, np. Czy wartości wyliczenia są koniecznie zapakowane. Kod OTOH, który działa z bazą danych, prawdopodobnie mimo wszystko jest powiązany z We / Wy, a identyfikatory w pudełkach nie będą stanowić żadnego wąskiego gardła.

W idealnym świecie identyfikatory byłyby niezmienne i porównywalne jedynie pod względem równości; rzeczywiste bajty byłyby szczegółami implementacji. Niepowiązane identyfikatory miałyby niezgodne typy, więc przez pomyłkę nie można było użyć jednego dla drugiego. Twoje rozwiązanie praktycznie pasuje do rachunku.

9000
źródło
-1

Nie możesz tego zrobić, wyliczenia muszą być predefiniowane. Więc jeśli nie chcesz wstępnie zdefiniować całego spektrum możliwych wartości identyfikatorów klientów, Twój typ będzie bezużyteczny.

Jeśli użyjesz, przeciwstawisz się swojemu głównemu celowi, jakim jest zapobieganie przypadkowemu przypisaniu identyfikatora typu A do identyfikatora typu B. Tak więc wyliczenia nie są pomocne w twoim celu.

Pojawia się jednak pytanie, czy pomocne byłoby utworzenie typów dla identyfikatora klienta, identyfikatora zamówienia i identyfikatora produktu poprzez zejście z Int32 (lub Guid). Mogę wymyślić powody, dla których byłoby to niewłaściwe.

Celem identyfikatora jest identyfikacja. Typy integralne są już do tego idealne, nie można ich już zidentyfikować po zejściu z nich. Specjalizacja nie jest wymagana ani pożądana, twój świat nie będzie reprezentowany lepiej dzięki różnym typom identyfikatorów. Więc z perspektywy OO byłoby źle.

Z twoją sugestią chcesz czegoś więcej niż bezpieczeństwa typu, chcesz cenić bezpieczeństwo i to jest poza dziedziną modelowania, to jest problem operacyjny.

Martin Maat
źródło
1
Nie, wyliczenia nie muszą być wstępnie zdefiniowane w .NET. Chodzi o to, że rzutowanie prawie nigdy się nie zdarza, np. W przypadku public ActionResult GetCustomer(CustomerId custId)rzutowania nie jest potrzebne - framework MVC dostarczy wartość.
default.kramer
2
Nie zgadzam się Dla mnie ma to sens z punktu widzenia OO. Int32 nie ma semantyki, jest to czysto „techniczny” typ. Ale potrzebuję abstrakcyjnego typu Identyfikatora, który służy do identyfikacji obiektów i który zdarza się zaimplementować jako Int32 (ale może być długi lub cokolwiek, to nie ma znaczenia). Mamy konkretne specjalizacje, takie jak ProductId, które pochodzą z identyfikatora, które identyfikują konkretne wystąpienie ProductId. Przypisywanie wartości OrderItemId do ProductId nie ma logicznego sensu (jest to oczywiście błąd), więc świetnie, że system typów może nas przed tym chronić.
qbd
1
Nie wiedziałem, że C # był tak elastyczny, jeśli chodzi o użycie wyliczeń. Z pewnością jest to dla mnie mylące jako programista przyzwyczajony do faktu (?), Że wyliczenia są, cóż, wyliczeniami możliwych wartości. Zazwyczaj określają zakres nazwanych identyfikatorów. Więc teraz ten typ jest „nadużywany” w tym sensie, że nie ma zasięgu i nie ma nazw, po prostu typ zostaje. I najwyraźniej ten typ też nie jest taki bezpieczny. Inna sprawa: przykładem jest oczywiście aplikacja bazy danych, a w pewnym momencie twoje „wyliczenie” zostanie zmapowane na pole typu integralnego, w którym to momencie bezpiecznie stracisz przypuszczalny typ.
Martin Maat,