Kiedy prymitywna obsesja nie jest zapachem kodu?

22

Ostatnio czytałem wiele artykułów opisujących prymitywną obsesję jako zapach kodu.

Są dwie zalety unikania prymitywnej obsesji:

  1. Sprawia, że ​​model domeny jest bardziej wyraźny. Na przykład mogę porozmawiać z analitykiem biznesowym na temat kodu pocztowego zamiast ciągu zawierającego kod pocztowy.

  2. Cała walidacja odbywa się w jednym miejscu zamiast w całej aplikacji.

Istnieje wiele artykułów opisujących zapach zapachowy. Na przykład widzę zaletę usuwania prymitywnej obsesji na punkcie takiego kodu pocztowego:

public class Address
{
    public ZipCode ZipCode { get; set; }
}

Oto konstruktor ZipCode:

public ZipCode(string value)
    {
        // Perform regex matching to verify XXXXX or XXXXX-XXXX format
        _value = value;
    }

Można byłoby złamanie DRY zasadę oddanie tej logiki walidacji wszędzie jest używany kod pocztowy.

A co z następującymi obiektami:

  1. Data urodzenia: sprawdź, czy to więcej niż umysł i mniej niż dzisiejsza data.

  2. Wynagrodzenie: sprawdź, czy jest większa lub równa zero.

Czy stworzyłbyś obiekt DateOfBirth i obiekt wynagrodzenia? Zaletą jest to, że możesz o nich mówić, opisując model domeny. Jest to jednak przypadek nadinżynierii, ponieważ nie ma zbyt wiele uzasadnień. Czy istnieje reguła, która opisuje, kiedy i kiedy nie należy usuwać prymitywnej obsesji, czy zawsze należy to robić, jeśli to możliwe?

Myślę, że mógłbym utworzyć alias typu zamiast klasy, co pomogłoby w punkcie 1 powyżej.

w0051977
źródło
8
„Łamałbyś zasadę DRY, umieszczając tę ​​logikę sprawdzania poprawności wszędzie tam, gdzie używany jest kod pocztowy”. To nie jest prawda. Walidacja powinna zostać wykonana natychmiast po wprowadzeniu danych do modułu . Jeśli istnieje więcej niż jeden „punkt wejścia”, walidacja powinna odbywać się w jednostce wielokrotnego użytku , i nie musi to być (ani nie powinno być) DTO ...
Timothy Truckle
1
W jaki sposób podajesz konstruktorowi „umysł” i „dzisiejszą datę” DateOfBirth, aby sprawdził w porównaniu z?
Caleth
11
Kolejną zaletą tworzenia niestandardowych typów jest bezpieczeństwo typów. Jeśli masz Salaryi Distancesprzeciwisz się, nie możesz przypadkowo użyć ich zamiennie. Możesz, jeśli oba są typu double.
Scroog1
3
@ w0051977 Twoje oświadczenie (jak rozumiem) sugeruje, że cokolwiek innego niż sprawdzenie poprawności w konstruktorze DTOs naruszy DRY. W rzeczywistości walidacja powinna odbywać się poza DTO ...
Timothy Truckle
2
Dla mnie to wszystko jest kwestią zakresu. Jeśli dasz prymitywom szeroki zakres, istnieje wiele sposobów, w jakie można je niewłaściwie wykorzystywać i źle traktować. Ogólnie rzecz biorąc, chcesz nadać im węższy zakres, a jednym ze sposobów jest zaprojektowanie klasy reprezentującej koncepcję przy użyciu prymitywnej, prywatnie przechowywanej jako wewnętrznej, w celu jej wdrożenia. Teraz zakres prymitywów jest wąski i jest mało prawdopodobne, aby był niewłaściwie wykorzystywany / źle traktowany, i możesz skutecznie utrzymywać niezmienniki. Ale jeśli zakres operacji pierwotnej byłby wąski, może to być przesada i wprowadzić wiele dodatkowych sprzężeń i kodu do utrzymania.

Odpowiedzi:

17

Primitive Obsession wykorzystuje pierwotne typy danych do reprezentowania pomysłów na domeny.

Przeciwieństwem byłoby „modelowanie domen”, a może „ponad inżynieria”.

Czy stworzyłbyś obiekt DateOfBirth i obiekt wynagrodzenia?

Wprowadzenie obiektu wynagrodzenia może być dobrym pomysłem z następującego powodu: liczby rzadko występują samodzielnie w modelu domeny, prawie zawsze mają wymiar i jednostkę. Zwykle nie modelujemy niczego przydatnego, jeśli dodamy długość do czasu lub masy i rzadko osiągamy dobre wyniki, gdy mierzymy metry i stopy.

Co do DateOfBirth, prawdopodobnie - należy rozważyć dwie kwestie. Po pierwsze, utworzenie nieprymitywnej daty daje miejsce, w którym można skoncentrować wszystkie dziwne obawy związane z matematyką randkową. Wiele języków zapewnia jeden gotowy; DateTime , java.util.Date . Są to domeny agnostyk implementacje datami, ale są one nie prymitywy.

Po drugie, DateOfBirthnie jest tak naprawdę datą; tutaj, w Stanach Zjednoczonych, „data urodzenia” jest konstrukcją kulturową / fikcją prawną. Zwykle mierzymy datę urodzenia od lokalnej daty urodzenia osoby; Bob, urodzony w Kalifornii, może mieć „wcześniejszą” datę urodzenia niż Alice, urodzona w Nowym Jorku, mimo że jest młodszy z nich.

Czy istnieje reguła, która opisuje, kiedy i kiedy nie należy usuwać prymitywnej obsesji, czy też powinieneś zawsze to robić, jeśli to możliwe.

Z pewnością nie zawsze; na granicach aplikacje nie są zorientowane obiektowo . Dość często spotyka się prymitywy używane do opisywania zachowań w testach .

VoiceOfUnreason
źródło
1
Pierwszy komentarz po cytacie na górze wydaje się być non-sequitur. Ponadto po prostu ponownie przedstawia temat pytania. W przeciwnym razie to dobra odpowiedź, ale uważam, że to naprawdę rozprasza.
JimmyJames
ani C # DateTime, ani java.util.Date nie są odpowiednimi typami bazowymi dla DateOfBirth.
kevin cline,
Może zastąpić java.util.Datezjava.time.LocalDate
Koray Tugay
7

Szczerze mówiąc: to zależy.

Zawsze istnieje ryzyko nadmiernej inżynierii kodu. Jak szeroko rozpowszechniony będzie DateOfBirth i Wynagrodzenie? Czy użyjesz ich tylko w trzech ściśle powiązanych klasach, czy będą one używane w całej aplikacji? Czy „po prostu” zamkniesz je we własnym typie / klasie, aby wymusić to jedno ograniczenie, czy może wymyślisz więcej ograniczeń / funkcji, które faktycznie tam są?

Weźmy na przykład Wynagrodzenie: Czy masz jakieś operacje z „Wynagrodzeniem” (np. Obsługa różnych walut, a może funkcja toString ())? Zastanów się, czym jest / robi wynagrodzenie, jeśli nie postrzegasz go jako zwykłego prymitywu, i istnieje duża szansa, że ​​wynagrodzenie będzie jego własną klasą.

CharonX
źródło
Czy alias typu jest dobrą alternatywą?
w0051977
@ w0051977 Zgadzam się z charonx i pseudonim może być alternatywą
techagrammer
@ w0051977 alias typu może być alternatywą, jeśli głównym celem jest wymuszenie ścisłego pisania, wyraźne określenie, jaką określoną wartością jest (Wynagrodzenie), aby uniknąć przypadkowego przypisania „zmiennych dolarów” (za godzinę? Tydzień? Miesiąc?) do „zmienna pensja” (na miesiąc? Rok?). To zależy od twoich potrzeb.
CharonX
@CharonX, uważam, że po przecinku powinna być używana liczba dziesiętna, a nie zmienna. Czy sie zgadzasz?
w0051977
@ w0051977 Jeśli masz dobry typ po przecinku, to ten byłby lepszy, tak. (Obecnie pracuję nad projektem C ++, więc logiczne liczby całkowite i zmiennoprzecinkowe są na pierwszym planie)
CharonX
5

Możliwa zasada może zależeć od warstwy programu. W przypadku domeny (DDD), czyli Entities Layer (Martin, 2018), może to również oznaczać „unikanie prymitywów dla wszystkiego, co reprezentuje koncepcję domeny / biznesu”. Uzasadnienia są takie, jak stwierdzono w PO: bardziej ekspresyjny model domeny, walidacja reguł biznesowych, wyraźne wyrażanie domyślnych pojęć (Evans, 2004).

Alias ​​typu może być lekką alternatywą (Ghosh, 2017) i w razie potrzeby przekształcony w klasę encji. Na przykład, możemy najpierw wymagać SalaryBE >=0, a później decydują się na nie pozwolić $100.33333, a wszystko powyżej $10,000,000(co bankructwa klienta). Użycie Nonnegativeprymitywnego przedstawienia Salaryi innych pojęć skomplikowałoby to refaktoryzację.

Unikanie prymitywów może również pomóc uniknąć nadmiernej inżynierii. Załóżmy, że musimy połączyć wynagrodzenie i datę urodzenia w strukturę danych: np. Aby mieć mniej parametrów metody lub przekazywać dane między modułami. Następnie możemy użyć krotki z typem (Salary, DateOfBirth). Rzeczywiście, krotka z prymitywami, (Nonnegative, Nonnegative)jest mało pouczająca, podczas gdy niektóre rozdęte class EmployeeDataukryłyby między innymi wymagane pola. Podpis w słowie calcPension(d: (Salary, DateOfBirth))jest bardziej skoncentrowany niż w calcPension(d: EmployeeData), co narusza zasadę segregacji interfejsu. Podobnie wyspecjalizowanie class SalaryAndDateOfBirthwydaje się niezręczne i prawdopodobnie jest przesadą. Później możemy zdefiniować klasę danych; krotki i typy domen elementarnych pozwalają nam odroczyć takie decyzje.

W warstwie zewnętrznej (np. GUI) sensowne może być „rozebranie” bytów do ich składowych prymitywów (np. Umieszczenie w DAO). Zapobiega to wyciekaniu abstrakcji domen do warstw zewnętrznych, jak zaleca Martin (2018).

Literatura
E. Evans, „Domain-Driven Design”, 2004
D. Ghosh, „Funkcjonalne i reaktywne modelowanie domen”, 2017
RC Martin, „Czysta architektura”, 2018

Tupolew._
źródło
+1 dla wszystkich referencji.
w0051977
4

Lepiej cierpisz z powodu pierwotnej obsesji lub bycia astronautą architektury ?

Oba przypadki są patologiczne, w jednym przypadku masz zbyt mało abstrakcji, co prowadzi do powtórzeń i łatwo mylnie jabłko z pomarańczą, w drugim zapomniałeś już o tym przestać i zaczynasz robić rzeczy, co utrudnia wykonanie czegokolwiek .

Jak prawie zawsze chcesz umiaru, który jest dobrze przemyślanym środkiem.

Pamiętaj, że właściwość ma oprócz nazwy także nazwę. Również rozkładanie adresu na części składowe może być zbyt restrykcyjne, jeśli zawsze odbywa się w ten sam sposób. Nie cały świat jest w centrum Nowego Jorku.

Deduplikator
źródło
3

Jeśli masz klasę wynagrodzeń, może ona mieć metody takie jak ApplyRaise.

Z drugiej strony klasa ZipCode nie musi mieć wewnętrznej walidacji, aby uniknąć powielania walidacji wszędzie tam, gdzie można mieć klasę ZipCodeValidator, którą można wstrzyknąć, więc jeśli twój system ma działać zarówno na adresach w USA, jak i w Wielkiej Brytanii, możesz po prostu wstrzyknąć poprawny walidator, a kiedy musisz również obsługiwać adresy AUS, możesz po prostu dodać nowy walidator.

Innym problemem jest to, że jeśli musisz zapisywać dane do bazy danych za pomocą EntityFramework, będzie musiał wiedzieć, jak obsługiwać wynagrodzenie lub kod pocztowy.

Nie ma jednoznacznej odpowiedzi na pytanie, gdzie należy wyznaczyć granicę między tym, jak inteligentne powinny być klasy, ale powiem, że mam tendencję do przenoszenia logiki biznesowej, takiej jak sprawdzanie poprawności, do klas logiki biznesowej, w których klasy danych są czystymi danymi, jak się wydaje pracować lepiej z EntityFramework.

Jeśli chodzi o użycie aliasów typów, nazwa członka / właściwości powinna zawierać wszystkie potrzebne informacje na temat zawartości, więc nie użyłbym aliasów typu.

Zgięty
źródło
Czy alias typu jest dobrą alternatywą?
w0051977
2

(Jakie jest prawdopodobnie pytanie)

Kiedy użycie typu pierwotnego nie jest zapachem kodu?

(Odpowiedź)

Jeśli parametr nie zawiera reguł - użyj typu pierwotnego.

Użyj prymitywnego typu dla takich jak:

htmlEntityEncode(string value)

Użyj obiektu do:

numberOfDaysSinceUnixEpoch(SimpleDate value)

Ten ostatni przykład mieć w nim zasady, czyli obiekt SimpleDateskłada się z Year, Month, i Day. Dzięki zastosowaniu Object w tym przypadku pojęcie SimpleDateważności może być zawarte w obiekcie.

Stoked PHP Engineer
źródło
1

Oprócz kanonicznych przykładów adresów e-mail lub kodów pocztowych podanych gdzie indziej w tym pytaniu, w przypadku gdy uważam, że refaktoryzacja z dala od Primitive Obsession może być szczególnie pomocna, to identyfikatory jednostek (patrz https://andrewlock.net/using-strongly-typed-entity -ids-to-unikać-prymitywnej-obsesji-część-1 / na przykład, jak to zrobić w .NET).

Nie pamiętam, ile razy błąd wkradł się, ponieważ metoda miała taką sygnaturę:

int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
  // implementation here
}

Kompiluje się dobrze, a jeśli nie jesteś rygorystyczny w testowaniu jednostek, może to również przejść pomyślnie. Jednak przekształć te identyfikatory jednostek w klasy właściwe dla domeny i hej, błędy w czasie kompilacji:

LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
  // implementation here
}

Wyobraź sobie, że ta metoda przeskalowała do 10 lub więcej parametrów, wszystkich inttypów danych (nie wspominając o zapachu kodu długiej listy parametrów ). Sytuacja staje się jeszcze gorsza, gdy używasz czegoś takiego jak AutoMapper do przełączania między obiektami domeny i DTO, i robisz refaktoryzację nie zostaje wykryty przez automatyczne mapowanie.

David Keaveny
źródło
0

Łamałbyś zasadę DRY, umieszczając tę ​​logikę sprawdzania poprawności wszędzie tam, gdzie używany jest kod pocztowy.

Z drugiej strony, w kontaktach z wieloma różnymi krajami i ich różnymi systemami kodów pocztowych, oznacza to, że nie można zweryfikować kodu pocztowego, chyba że znasz dany kraj. Twoja ZipCodeklasa musi także przechowywać kraj.

Ale czy następnie oddzielnie przechowujesz kraj jako część Address(którego kod pocztowy jest również częścią) i część kodu pocztowego (do weryfikacji)?

  • Jeśli to zrobisz, naruszysz również SUCHO. Nawet jeśli nie nazwiesz tego naruszeniem OSUSZANIA (ponieważ każda instancja ma inny cel), nadal niepotrzebnie zajmuje dodatkową pamięć, oprócz otwierania drzwi do błędów, gdy dwie wartości kraju są różne (co logicznie nigdy nie powinno być).
    • Lub alternatywnie prowadzi to do konieczności zsynchronizowania dwóch punktów danych, aby upewnić się, że zawsze są one takie same, co sugeruje, że powinieneś naprawdę przechowywać te dane w jednym punkcie, a tym samym udaremnić cel.
  • Jeśli tego nie zrobisz, to nie jest to ZipCodeklasa, ale Addressklasa, która znowu będzie zawierała string ZipCodeco oznacza, że ​​zatoczyliśmy pełne koło.

Na przykład mogę porozmawiać z analitykiem biznesowym na temat kodu pocztowego zamiast ciągu zawierającego kod pocztowy.

Zaletą jest to, że możesz o nich mówić, opisując model domeny.

Nie rozumiem twojego podstawowego twierdzenia, że ​​gdy część informacji ma dany typ zmiennej, to w jakiś sposób masz obowiązek wspomnieć o tym typie, gdy rozmawiasz z analitykiem biznesowym.

Czemu? Dlaczego nie możesz po prostu mówić o „kodzie pocztowym” i całkowicie pomijać określony typ? Jakiego rodzaju rozmowy prowadzisz ze swoim analitykiem biznesowym (nie technicznym!), Gdzie typ nieruchomości jest kwintesencją rozmowy?

Skąd pochodzę, kody pocztowe są zawsze numeryczne. Mamy więc wybór, możemy przechowywać go jako intlub jako string. Zwykle używamy łańcucha, ponieważ nie oczekujemy operacji matematycznych na danych, ale nigdy analityk biznesowy nie powiedział mi, że musi to być łańcuch. Decyzję tę podejmuje deweloper (lub zapewne analityk techniczny, choć z mojego doświadczenia wynika, że ​​nie zajmują się bezpośrednio drobiazgami).

Analityk biznesowy nie dba o typ danych, o ile aplikacja wykonuje to, czego oczekuje.


Walidacja jest trudną bestią do rozwiązania, ponieważ zależy od tego, czego oczekują ludzie.

Po pierwsze, nie zgadzam się z argumentem walidacyjnym jako sposobem pokazania, dlaczego należy unikać prymitywnej obsesji, ponieważ nie zgadzam się z tym, że (jako uniwersalna prawda) dane muszą być zawsze weryfikowane przez cały czas.

Na przykład, co jeśli jest to bardziej skomplikowane wyszukiwanie? Zamiast zwykłego sprawdzenia formatu, co jeśli weryfikacja wymaga skontaktowania się z zewnętrznym interfejsem API i oczekiwania na odpowiedź? Czy naprawdę chcesz zmusić aplikację do wywołania tego zewnętrznego interfejsu API dla każdego ZipCodeobiektu, który tworzysz?
Może jest to ścisły wymóg biznesowy, a następnie oczywiście uzasadniony. Ale to nie jest uniwersalna prawda. Będzie wiele przypadków użycia, w których będzie to bardziej obciążenie niż rozwiązanie.

Jako drugi przykład podczas wpisywania adresu w formularzu często podajesz swój kod pocztowy przed krajem. Chociaż miło jest mieć natychmiastowe informacje zwrotne dotyczące sprawdzania poprawności w interfejsie użytkownika, w rzeczywistości byłoby przeszkodą dla mnie (jako użytkownika), jeśli aplikacja powiadomiłaby mnie o „złym” formacie kodu pocztowego, ponieważ prawdziwym źródłem problemu jest (np.) To, że mój kraj nie jest krajem wybranym domyślnie, dlatego weryfikacja nastąpiła dla niewłaściwego kraju.
To zły komunikat o błędzie, który rozprasza użytkownika i powoduje niepotrzebne zamieszanie.

Podobnie jak wieczna walidacja nie jest uniwersalną prawdą, podobnie jak moje przykłady. To jest kontekstowe . Niektóre domeny aplikacji wymagają przede wszystkim weryfikacji danych. Inne domeny nie umieszczają walidacji tak wysoko na liście priorytetów, ponieważ problemy, które niesie ze sobą, są sprzeczne z ich rzeczywistymi priorytetami (np. Wrażenia użytkownika lub możliwość początkowego przechowywania wadliwych danych, aby można je było poprawić zamiast nigdy nie pozwalać na to przechowywane)

Data urodzenia: sprawdź, czy to więcej niż umysł i mniej niż dzisiejsza data.
Wynagrodzenie: sprawdź, czy jest większa lub równa zero.

Problem z tymi walidacjami polega na tym, że są one niekompletne, zbędne lub wskazują na znacznie większy problem .

Sprawdzanie, czy data jest większa niż umysł, jest zbędne. Umysł dosłownie oznacza, że ​​jest to najmniejsza możliwa data. Poza tym, gdzie narysujesz linię istotności? Po co zapobiegać, DateTime.MinDateale pozwalać DateTime.MinDate.AddSeconds(1)? Wybierasz konkretną wartość, która nie jest szczególnie zła w porównaniu do wielu innych wartości.

Moje urodziny są 2 stycznia 1978 r. (Nie, ale załóżmy, że tak). Powiedzmy jednak, że dane w Twojej aplikacji są nieprawidłowe, a zamiast tego napisano, że moje urodziny to:

  • 1 stycznia 1978 r
  • 1 stycznia 1722 r
  • 1 stycznia 2355 r

Wszystkie te daty są nieprawidłowe. Żaden z nich nie jest „bardziej odpowiedni” niż drugi. Ale twoja reguła sprawdzania poprawności złapie tylko jeden z tych trzech przykładów.

Całkowicie pominąłeś także kontekst korzystania z tych danych. Jeśli jest to używane np. W bocie przypominającym o urodzinach, powiedziałbym, że walidacja nie ma sensu, ponieważ nie ma szczególnych złych konsekwencji dla podania niewłaściwej daty.
Z drugiej strony, jeśli są to dane rządowe i potrzebujesz daty urodzenia, aby uwierzytelnić czyjąś tożsamość (a ich niepodanie prowadzi do złych konsekwencji, np. Odmowy zabezpieczenia społecznego), to poprawność danych jest najważniejsza i musisz w pełni sprawdź poprawność danych. Proponowana walidacja, którą masz teraz, jest nieodpowiednia.

W przypadku wynagrodzenia istnieje pewien zdrowy rozsądek, ponieważ nie może być ujemny. Ale jeśli realistycznie oczekujesz, że wprowadzane są bezsensowne dane, sugerowałbym zbadanie źródła tych bezsensownych danych. Ponieważ jeśli nie można im ufać, że wprowadzają sensowne dane, nie można im również ufać, że wprowadzą prawidłowe dane.

Jeśli zamiast tego wynagrodzenie jest obliczane przez aplikację i w jakiś sposób możliwe jest uzyskanie liczby ujemnej (i poprawnej), wówczas lepszym rozwiązaniem byłoby Math.Max(myValue, 0)przekształcenie liczb ujemnych w 0, zamiast nieudanej weryfikacji. Ponieważ jeśli twoja logika zdecydowała, że ​​wynik jest liczbą ujemną, nieudana walidacja oznacza, że ​​będzie musiała powtórzyć obliczenia, i nie ma powodu sądzić, że za drugim razem pojawi się inna liczba.
A jeśli pojawi się inna liczba, ponownie podejrzewasz, że obliczenia nie są spójne i dlatego nie można im ufać.

Nie oznacza to, że sprawdzanie poprawności nie jest przydatne. Ale bezcelowa walidacja jest zła, zarówno dlatego, że wymaga wysiłku, ale tak naprawdę nie rozwiązuje problemu, i daje ludziom fałszywe poczucie bezpieczeństwa.

Flater
źródło
Czyjaś data urodzenia może faktycznie przekroczyć bieżącą datę, jeśli dziecko urodzi się teraz w strefie czasowej, która już przeskoczyła na następny dzień. A szpital może przechowywać „spodziewaną datę urodzenia” w bazie danych, która może być miesiącami w przyszłości. Czy chciałbyś do tego innego typu?
gnasher729
@ gnasher729: Nie jestem do końca pewien, czy się zgadzam, wygląda na to, że zgadzasz się ze mną (walidacja jest kontekstowa i ogólnie niepoprawna), ale sformułowanie komentarza sugeruje, że uważasz, że się nie zgadzam. A może źle czytam?
Flater