Prymitywna vs klasa reprezentująca prosty obiekt domeny?

14

Jakie są ogólne wytyczne lub praktyczne zasady korzystania z obiektu specyficznego dla domeny w porównaniu do zwykłego ciągu lub liczby?

Przykłady:

  • Klasa wiekowa a liczba całkowita?
  • Klasa FirstName vs String?
  • UniqueID vs String
  • Klasa PhoneNumber vs String vs Long?
  • Klasa DomainName vs String?

Myślę, że większość praktykujących OOP zdecydowanie powiedziałaby określone klasy dla PhoneNumber i DomainName. Im więcej zasad dotyczących tego, co czyni je ważnymi i jak je porównywać, łatwiej i bezpieczniej radzić sobie z prostymi klasami. Ale w przypadku pierwszych trzech jest więcej debat.

Nigdy nie spotkałem się z klasą „Age”, ale można argumentować, że ma to sens, biorąc pod uwagę, że musi być ona nieujemna (okej, wiem, że możesz się kłócić o ujemny wiek, ale to dobry przykład, że jest prawie równoważna prymitywnej liczbie całkowitej).

Ciąg znaków często reprezentuje „Imię”, ale nie jest doskonały, ponieważ pusty ciąg znaków jest prawidłowym ciągiem znaków, ale nie jest prawidłową nazwą. Porównanie zwykle odbywa się z pominięciem wielkości liter. Pewnie, że istnieją metody sprawdzania pustości, porównywania bez rozróżniania wielkości liter itp., Ale wymaga tego konsument.

Czy odpowiedź zależy od środowiska? Przede wszystkim interesuje mnie oprogramowanie o wysokiej wartości dla przedsiębiorstw, które będzie żyło i będzie utrzymywane przez ponad dekadę.

Być może zastanawiam się nad tym, ale naprawdę chciałbym wiedzieć, czy ktoś ma zasady dotyczące wyboru klasy zamiast prymitywnej.

noctonura
źródło
2
Klasa wiekowa nie jest dobrym pomysłem, jeśli można ją zastąpić liczbą całkowitą. Jeśli konstruktor wymaga daty urodzenia i ma metody getCurrentAge () i getAgeAt (data daty), to klasa ma znaczenie. Ale nie można go zastąpić liczbą całkowitą.
kiwiron
5
„Łańcuch czasami nie jest łańcuchem. Modeluj go odpowiednio.”. Dobry post Mark Seemann o „prymitywnej obsesji”: blog.ploeh.dk/2015/01/19/…
Serhii Shushliapin
4
W rzeczywistości klasa Age ma dziwny sens. Powszechna zachodnia definicja wieku wynosi zero w chwili urodzenia i dodaje 1 w następne urodziny. Metodologia wschodnioazjatycka polega na tym, że masz 1 w chwili urodzenia, a 1 dodaje się w następnym dniu Nowego Roku. W związku z tym w zależności od lokalizacji wiek można obliczyć inaczej. EG z en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
Peter M
@PeterM, to dobra informacja! Daty / godziny, a teraz wiek. Powinny być prostymi pojęciami, ale dowodzi to, że na świecie jest o wiele bardziej złożona niż to, do czego jesteśmy przyzwyczajeni.
Machado
6
Widzę dużo pisania poniżej tego pytania, ale odpowiedź nie jest tak skomplikowana. Używasz Ageklasy zamiast liczby całkowitej, gdy potrzebujesz dodatkowego zachowania zapewnianego przez taką klasę.
Robert Harvey

Odpowiedzi:

20

Jakie są ogólne wytyczne lub praktyczne zasady korzystania z obiektu specyficznego dla domeny w porównaniu do zwykłego ciągu lub liczby?

Ogólna zasada mówi, że chcesz modelować swoją domenę w języku specyficznym dla domeny.

Zastanów się: dlaczego używamy liczby całkowitej? Równie łatwo możemy przedstawić wszystkie liczby całkowite za pomocą łańcuchów. Lub z bajtami.

Gdybyśmy programowali w języku agnostycznym domenowym, który zawierałby prymitywne typy liczb całkowitych i wieku, co byś wybrał?

To, co tak naprawdę sprowadza się do „prymitywnej” natury niektórych typów, to wypadek wyboru języka do naszej implementacji.

W szczególności liczby zwykle wymagają dodatkowego kontekstu. Wiek to nie tylko liczba, ale także wymiar (czas), jednostki (lata?), Reguły zaokrąglania! Łączenie wieków razem ma sens w taki sposób, że nie dodaje się wieku do pieniędzy.

Wyróżnienie typów pozwala nam modelować różnice między niezweryfikowanym adresem e-mail a zweryfikowanym adresem e-mail .

Przypadek przedstawienia tych wartości w pamięci jest jedną z najmniej interesujących części. Model domeny nie dba CustomerIdo int, ani o String, ani o identyfikator UUID / GUID, ani o węzeł JSON. Chce tylko afordancji.

Czy naprawdę obchodzi nas, czy liczby całkowite są dużymi czy małymi? Czy obchodzi nas, czy to, Listco minęło, jest abstrakcją nad tablicą lub wykresem? Kiedy odkryjemy, że arytmetyka podwójnej precyzji jest nieefektywna i że musimy przejść na reprezentację zmiennoprzecinkową, czy model domeny powinien się tym przejmować ?

Parnas w 1972 roku napisał

Zamiast tego proponujemy, aby rozpocząć od listy trudnych decyzji projektowych lub decyzji projektowych, które mogą ulec zmianie. Każdy moduł jest następnie zaprojektowany tak, aby ukryć taką decyzję przed innymi.

W pewnym sensie wprowadzane przez nas typy wartości specyficzne dla domeny to moduły, które izolują naszą decyzję o tym, jaka podstawowa reprezentacja danych powinna zostać użyta.

Zaletą jest więc modułowość - otrzymujemy projekt, w którym łatwiej jest zarządzać zakresem zmiany. Minusem jest koszt - więcej pracy wymaga stworzenie potrzebnych typów na zamówienie, wybór odpowiednich typów wymaga głębszego zrozumienia domeny. Ilość pracy wymagana do utworzenia modułu wartości będzie zależeć od lokalnego dialektu Blub .

Inne terminy w równaniu mogą obejmować oczekiwany czas życia rozwiązania (staranne modelowanie skryptów, które zostaną uruchomione, gdy będzie miał kiepski zwrot z inwestycji), jak blisko domeny jest do podstawowych kompetencji firmy.

Jednym szczególnym przypadkiem, który możemy rozważyć, jest komunikacja ponad granicami . Nie chcemy znajdować się w sytuacji, w której zmiany w jednej przenośnej jednostce wymagają skoordynowanych zmian z innymi mobilnymi jednostkami. Dlatego wiadomości zwykle koncentrują się bardziej na reprezentacjach, bez uwzględnienia niezmienników lub zachowań specyficznych dla domeny. Nie będziemy próbować komunikować „ta wartość musi być ściśle dodatnia” w formacie komunikatu, ale raczej komunikować jego reprezentację w sieci i zastosować weryfikację tej reprezentacji na granicy domeny.

VoiceOfUnreason
źródło
Doskonała uwaga na temat ukrywania (lub abstrakcjonowania) tych rzeczy, które są trudne i mogą się zmienić.
user949300,
4

Myślisz o abstrakcji i to świetnie. Jednak rzeczy, które twoja lista wydaje mi się w większości indywidualnymi atrybutami czegoś większego (oczywiście nie wszystkie tego samego).

To, co uważam za ważniejsze, to zapewnienie abstrakcji, która grupuje atrybuty razem i daje im odpowiedni dom, na przykład klasę Person, aby programista klienta nie musiał radzić sobie z wieloma atrybutami, a raczej z abstrakcją wyższego poziomu.

Po uzyskaniu tej abstrakcji niekoniecznie potrzebujesz dodatkowych abstrakcji dla atrybutów, które są tylko wartościami. Klasa Person może przechowywać datę urodzenia jako (pierwotną) datę, wraz z PhoneNumbers jako listą (prymitywnych) ciągów znaków i Nazwą jako (pierwotny) ciąg znaków.

Jeśli masz numer telefonu z kodem formatującym, teraz są to dwa atrybuty i zasługiwałaby na klasę dla numeru telefonu (ponieważ prawdopodobnie miałby również pewne zachowanie, takie jak tylko uzyskiwanie liczb vs. uzyskiwanie sformatowanego numeru telefonu).

Jeśli masz Nazwę jako wiele atrybutów (np. Prefiksy / tytuły, pierwszy, ostatni, sufiks), które zasługują na klasę (ponieważ teraz masz pewne zachowania, takie jak uzyskanie pełnej nazwy jako ciągu vs. uzyskiwanie mniejszych elementów, tytułu itp. .).

Erik Eidt
źródło
1

Powinieneś modelować tak, jak potrzebujesz, jeśli chcesz po prostu danych, możesz użyć typów prymitywnych, ale jeśli chcesz wykonać więcej operacji na tych danych, należy je modelować w określonych klasach, na przykład (zgadzam się z @kiwiron w jego komentarzu) Klasa wiekowa nie ma sensu, ale lepiej byłoby zastąpić ją klasą Data urodzenia i umieść tam te metody.

Zaletą modelowania tych pojęć w klasach jest wymuszenie bogactwa swojego modelu, na przykład, zamiast tego masz metodę, która akceptuje dowolną datę, użytkownik może wypełnić ją dowolną datą, datą rozpoczęcia II wojny światowej lub datą zakończenia zimnej wojny, z tych dat wciąż ważna jest data urodzenia. możesz zastąpić go datą urodzenia, dlatego w takim przypadku narzucisz metodę akceptowania daty urodzenia, aby nie akceptować żadnej daty.

W przypadku Imienia nie widzę wiele korzyści, także UniqueID, PhoneNumber trochę, możesz poprosić go o podanie kraju i regionu, na przykład ponieważ ogólnie PhoneNumber odnosi się do krajów i regionów, niektórzy operatorzy używają predefiniowanych numerów Sufiksów do klasyfikowania telefonu komórkowego , Office ... numery, dzięki czemu możesz dodać te metody do tej klasy, tak samo jak nazwa_domeny.

Aby uzyskać więcej informacji, polecam przyjrzeć się obiektom Value w DDD (Domain Driven Design).

La VloZ Merrill
źródło
1

To zależy od tego, czy masz zachowanie (które musisz zachować i / lub zmienić) związane z tymi danymi, czy nie.

Krótko mówiąc, jeśli jest to tylko fragment danych, który jest rzucany bez większego powiązania z zachowaniem, użyj prymitywnego typu lub, w przypadku nieco bardziej złożonych fragmentów, struktury danych (w dużej mierze bez zachowania) (np. Klasa zawierająca tylko metody pobierające i setery).

Jeśli z drugiej strony okaże się, że w celu podjęcia decyzji sprawdzasz lub manipulujesz tymi danymi w różnych miejscach kodu, miejscach często nie blisko siebie - następnie zamień je w odpowiedni obiekt poprzez zdefiniowanie klasy i umieszczenie w nim odpowiedniego zachowania jako metod. Jeśli z jakiegoś powodu uważasz, że zdefiniowanie takiej klasy nie ma sensu, alternatywą jest skonsolidowanie tego zachowania w pewnego rodzaju klasę „usługową”, która działa na tych danych.

Filip Milovanović
źródło
1

Twoje przykłady wyglądają bardziej jak właściwości lub atrybuty czegoś innego. Oznacza to, że całkowicie uzasadnione byłoby rozpoczęcie od samego prymitywu. W pewnym momencie będziesz musiał użyć prymitywu, więc zwykle oszczędzam złożoność na czas, gdy jej naprawdę potrzebuję.

Załóżmy na przykład, że tworzę grę i nie muszę się martwić o starzenie się postaci. Zastosowanie do tego liczby całkowitej ma sens. Wiek można wykorzystać w prostych kontrolach, aby sprawdzić, czy postać może coś zrobić, czy nie.

Jak zauważyli inni, wiek może być skomplikowanym tematem w zależności od lokalizacji. Jeśli musisz pogodzić wiek w różnych lokalizacjach itp., Warto wprowadzić Ageklasę, która ma DateOfBirthiLocale jak właściwości. Ta klasa wieku może również obliczyć aktualny wiek w dowolnym znaczniku czasu.

Istnieją więc powody, aby promować prymityw do klasy, ale są one bardzo specyficzne dla aplikacji. Oto kilka przykładów:

  • Masz specjalną logikę sprawdzania poprawności, która musi być stosowana wszędzie tam, gdzie używany jest typ informacji (np. Numery telefonów, adresy e-mail).
  • Masz specjalną logikę formatowania, która służy do renderowania dla ludzi do odczytu (np. Adresy IP4 i IP6)
  • Masz zestaw funkcji, które wszystkie odnoszą się do tego typu obiektu
  • Masz specjalne zasady porównywania podobnych typów wartości

Zasadniczo, jeśli masz wiele reguł dotyczących traktowania wartości, które chcesz konsekwentnie stosować w całym projekcie, utwórz klasę. Jeśli możesz żyć z prostymi wartościami, ponieważ nie ma to tak naprawdę decydującego znaczenia dla funkcjonalności, użyj prymitywu.

Berin Loritsch
źródło
1

Pod koniec dnia obiekt jest tylko świadkiem, że jakiś proces faktycznie został uruchomiony .

Prymityw intoznacza, że ​​niektóre procesy wyzerowały 4 bajty pamięci, a następnie przerzuciły bity niezbędne do przedstawienia liczby całkowitej w zakresie od -2 147 483 648 do 2 147 483 647; co nie jest mocnym stwierdzeniem na temat wieku. Podobnie (niepuste)System.String oznacza, że ​​niektóre bajty zostały przydzielone, a bity odwrócone, aby reprezentować sekwencję znaków Unicode w odniesieniu do niektórych kodowań.

Decydując się na reprezentację obiektu domeny, powinieneś pomyśleć o procesie, dla którego służy on jako świadek. Jeśli FirstNamenaprawdę powinien być niepustym ciągiem, powinien stać jako świadek, że system uruchomił jakiś proces, który zapewnia, że ​​co najmniej jeden znak spacji został utworzony w sekwencji znaków, które zostały ci przekazane.

Podobnie Ageobiekt prawdopodobnie powinien być świadkiem, że jakiś proces obliczył różnicę między dwiema datami. Odints ogół nie są wynikiem obliczenia różnicy między dwiema datami, są ipso faktycznie niewystarczające do przedstawienia wieku.

Jest więc oczywiste, że prawie zawsze chcesz utworzyć klasę (która jest tylko programem) dla każdego obiektu domeny. Więc w czym problem? Problem polega na tym, że C # (co kocham) i Java wymagają zbyt wiele ceremonii w tworzeniu klas. Możemy jednak wyobrazić sobie alternatywne składnie, które znacznie ułatwiłyby definiowanie obiektów domeny:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

Na przykład F # ma w tym celu ładną składnię w postaci aktywnych wzorców :

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

Chodzi o to, że tylko dlatego, że język programowania sprawia, że ​​kłopotliwe jest definiowanie obiektów domeny, nie oznacza, że ​​tak naprawdę jest to zły pomysł.


† Nazywamy te rzeczy również warunkami pocztowymi , ale wolę termin „świadek”, ponieważ służy on jako dowód na to, że jakiś proces może i był realizowany.

Rodrick Chapman
źródło
0

Pomyśl o tym, co zyskujesz, stosując klasę vs. prymityw. Jeśli korzystasz z klasy, możesz cię zdobyć

  • uprawomocnienie
  • formatowanie
  • inne przydatne metody
  • bezpieczeństwo typu (tj. z PhoneNumberklasą, nie powinno być dla mnie możliwe przypisanie go do łańcucha lub łańcucha losowego (tj. „ogórka”), gdzie oczekuję numeru telefonu)
  • wszelkie inne korzyści

a te korzyści ułatwiają życie, poprawiają jakość kodu, czytelność i przeważają nad bólem związanym z używaniem takich rzeczy, rób to. W przeciwnym razie trzymaj się prymitywów. Jeśli jest blisko, zadzwoń do sądu.

Zrozum także, że czasami dobre nazewnictwo może po prostu sobie z tym poradzić (tj. Ciąg nazwany firstNamekontra tworzenie całej klasy, która po prostu otacza właściwość łańcucha). Zajęcia nie są jedynym sposobem na rozwiązanie problemów.

Becuzz
źródło