Według Kiedy prymitywna obsesja nie jest zapachem kodu? , Powinienem utworzyć obiekt ZipCode reprezentujący kod pocztowy zamiast obiektu String.
Jednak z mojego doświadczenia wolę widzieć
public class Address{
public String zipCode;
}
zamiast
public class Address{
public ZipCode zipCode;
}
ponieważ myślę, że ten ostatni wymaga ode mnie przejścia do klasy ZipCode, aby zrozumieć program.
I uważam, że muszę przechodzić między wieloma klasami, aby zobaczyć definicję, jeśli wszystkie prymitywne pola danych zostaną zastąpione przez klasę, która wydaje się cierpieć z powodu problemu jo-jo (anty-wzorzec).
Chciałbym więc przenieść metody ZipCode do nowej klasy, na przykład:
Stary:
public class ZipCode{
public boolean validate(String zipCode){
}
}
Nowy:
public class ZipCodeHelper{
public static boolean validate(String zipCode){
}
}
tak więc tylko ten, kto musi zweryfikować kod pocztowy, będzie zależał od klasy ZipCodeHelper. I znalazłem kolejną „korzyść” z utrzymywania prymitywnej obsesji: sprawia, że klasa wygląda jak jej zserializowana forma, jeśli taka istnieje, na przykład: tablica adresowa z kolumną łańcuchową zipCode.
Moje pytanie brzmi: czy „unikanie problemu jo-jo” (przejście między definicjami klas) jest ważnym powodem pozwalającym na „prymitywną obsesję”?
źródło
Odpowiedzi:
Zakłada się, że nie trzeba jo-jo do klasy ZipCode, aby zrozumieć klasę Adres. Jeśli ZipCode jest dobrze zaprojektowany, powinno być oczywiste, co robi po przeczytaniu klasy Address.
Programy nie są odczytywane od końca do końca - zazwyczaj programy są zbyt złożone, aby to umożliwić. Nie możesz jednocześnie przechowywać całego kodu w programie. Używamy więc abstrakcji i enkapsulacji, aby „podzielić” program na znaczące jednostki, abyś mógł spojrzeć na jedną część programu (powiedzmy klasę Adres) bez konieczności odczytywania całego kodu, od którego zależy.
Na przykład jestem pewien, że nie odczytujesz jo-jo odczytywania kodu źródłowego dla String za każdym razem, gdy napotkasz String w kodzie.
Zmiana nazwy klasy z ZipCode na ZipCodeHelper sugeruje, że istnieją teraz dwie osobne koncepcje: kod pocztowy i pomocnik kodu pocztowego. Tak dwa razy bardziej skomplikowane. A teraz system typów nie może pomóc w odróżnieniu dowolnego ciągu od prawidłowego kodu pocztowego, ponieważ mają ten sam typ. W tym przypadku właściwa jest „obsesja”: sugerujesz bardziej złożoną i mniej bezpieczną alternatywę tylko dlatego, że chcesz uniknąć prostego typu opakowania wokół prymitywów.
Używanie prymitywów jest uzasadnione przez IMHO w przypadkach, gdy nie ma sprawdzania poprawności lub innej logiki w zależności od tego konkretnego typu. Ale gdy tylko dodasz dowolną logikę, jest o wiele prostsze, jeśli logika ta jest enkapsulowana z typem.
Jeśli chodzi o serializację, myślę, że to brzmi jak ograniczenie w używanym frameworku. Z pewnością powinieneś być w stanie serializować kod pocztowy do ciągu lub odwzorować go na kolumnę w bazie danych.
źródło
ZipCodeHelper
(do którego wolałbym zadzwonićZipCodeValidator
) może równie dobrze ustanowić połączenie z usługą internetową, aby wykonać swoją pracę. To nie byłoby częścią pojedynczej odpowiedzialności „przechowuj dane kodu pocztowego”. Uczynienie systemu typów niedozwoleniem niepoprawnych kodów pocztowych może być nadal osiągnięte poprzez uczynienieZipCode
konstruktora ekwiwalentem pakietu Java-prywatnym i wywołanie go za pomocą parametru,ZipCodeFactory
który zawsze wywołuje weryfikator.ZipCode
byłaby „strukturą danych” iZipCodeHelper
„obiekt”. W każdym razie uważam, że zgadzamy się, że nie powinniśmy przekazywać połączeń internetowych do konstruktora ZipCode.int
jako ID pozwala na pomnożenie ID przez ID ...)Foo
iFooValidator
klas. My mogliśmy miećZipCode
klasę, która sprawdza format iZipCodeValidator
że uderza jakąś usługę sieci Web, aby sprawdzić, czy prawidłowo sformatowanyZipCode
jest rzeczywiście obecny. Wiemy, że zmieniają się kody pocztowe. Ale praktycznie będziemy mieć listę poprawnych kodów pocztowych zawartych wZipCode
lub w lokalnej bazie danych.Jeśli można zrobić:
Konstruktor ZipCode:
Potem złamałeś enkapsulację i dodałeś dość niemądrą zależność do klasy ZipCode. Jeśli konstruktor nie wywołuje, oznacza
ZipCodeHelper.validate(...)
to, że masz wyodrębnioną logikę na własnej wyspie bez jej wymuszania. Możesz tworzyć nieprawidłowe kody pocztowe.validate
Metoda powinna być statyczna metoda na klasie kod pocztowy. Teraz znajomość „poprawnego” kodu pocztowego jest dołączona do klasy ZipCode. Biorąc pod uwagę, że twoje przykłady kodu wyglądają jak Java, konstruktor ZipCode powinien zgłosić wyjątek, jeśli podano nieprawidłowy format:Konstruktor sprawdza format i zgłasza wyjątek, zapobiegając w ten sposób tworzeniu nieprawidłowych kodów pocztowych, a
validate
metoda statyczna jest dostępna dla innego kodu, więc logika sprawdzania formatu jest zawarta w klasie ZipCode.W tym wariancie klasy ZipCode nie ma „jo-jo”. Nazywa się to po prostu właściwym programowaniem obiektowym.
Zignorujemy również internacjonalizację, co może wymagać innej klasy o nazwie ZipCodeFormat lub PostalService (np. PostalService.isValidPostalCode (...), PostalService.parsePostalCode (...) itp.).
źródło
ZipCode.validate
Metoda ta polega na sprawdzeniu wstępnym, które można wykonać przed wywołaniem konstruktora, który zgłosi wyjątek.Jeśli często zmagasz się z tym pytaniem, być może używany język nie jest odpowiednim narzędziem do pracy? Tego rodzaju „prymitywy typowane w domenie” są trywialnie łatwe do wyrażenia, na przykład w języku F #.
Tam możesz na przykład napisać:
Jest to bardzo przydatne w celu uniknięcia typowych błędów, takich jak porównywanie identyfikatorów różnych podmiotów. A ponieważ te prymitywne typy są znacznie lżejsze niż klasy C # lub Java, w końcu ich użyjesz.
źródło
ZipCode
?Odpowiedź zależy całkowicie od tego, co naprawdę chcesz zrobić z kodami pocztowymi. Oto dwie skrajne możliwości:
(1) Wszystkie adresy znajdują się w jednym kraju. W ogóle żadnych wyjątków. (Np. Żaden klient zagraniczny lub żaden pracownik, którego prywatny adres znajduje się za granicą, gdy pracują dla klienta zagranicznego.) Ten kraj ma kody pocztowe i można oczekiwać, że nigdy nie będą poważnie problematyczne (tj. Nie wymagają swobodnego wprowadzania danych np. „obecnie D4B 6N2, ale zmienia się co 2 tygodnie”). Kody pocztowe służą nie tylko do adresowania, ale także do sprawdzania poprawności informacji o płatności lub podobnych celów. - W tych okolicznościach klasa kodu pocztowego ma sens.
(2) Adresy mogą być w prawie każdym kraju, więc dziesiątki lub setki schematów adresowych z kodami pocztowymi lub bez nich (oraz z tysiącami dziwnych wyjątków i szczególnych przypadków) są istotne. Kod „ZIP” jest tak naprawdę proszony tylko o przypomnienie osobom z krajów, w których kody pocztowe są używane, aby nie zapomniały podać swoich. Adresy są używane tylko w taki sposób, że jeśli ktoś utraci dostęp do swojego konta i może udowodnić swoją nazwę i adres, dostęp zostanie przywrócony. - W tych okolicznościach klasy kodów pocztowych dla wszystkich odpowiednich krajów byłyby ogromnym wysiłkiem. Na szczęście w ogóle nie są potrzebne.
źródło
Inne odpowiedzi mówiły o modelowaniu domen OO i zastosowaniu bogatszego typu do reprezentowania twojej wartości.
Nie zgadzam się, szczególnie biorąc pod uwagę przykładowy kod, który opublikowałeś.
Ale zastanawiam się również, czy to faktycznie odpowiada tytułowi twojego pytania.
Rozważ następujący scenariusz (zaczerpnięty z faktycznego projektu, nad którym pracuję):
Masz zdalną aplikację na urządzeniu polowym, która komunikuje się z twoim serwerem centralnym. Jednym z pól DB wpisu urządzenia jest kod pocztowy adresu, pod którym znajduje się urządzenie polowe. Nie obchodzi Cię kod pocztowy (ani żaden inny adres adresu w tej sprawie). Wszyscy, którzy się tym przejmują, znajdują się po drugiej stronie granicy HTTP: akurat jesteś jedynym źródłem prawdy dla danych. Nie ma miejsca w modelowaniu domen. Po prostu go nagrywasz, zatwierdza, przechowuje i na żądanie przetasowuje w obiekcie blob JSON do innych miejsc.
W tym scenariuszu robienie czegokolwiek poza sprawdzaniem poprawności wstawki za pomocą ograniczenia wyrażenia regularnego SQL (lub jego odpowiednika ORM) jest prawdopodobnie przesadą w stosunku do odmiany YAGNI.
źródło
RegexValidatedString
, zawierający sam łańcuch i wyrażenie regularne użyte do jego sprawdzenia. Ale chyba, że każda instancja ma unikalny regex (co jest możliwe, ale mało prawdopodobne), wydaje się to trochę głupie i marnowanie pamięci (i być może czas kompilacji regex). Więc albo umieszczasz wyrażenie regularne w osobnej tabeli i zostawiasz klucz wyszukiwania w każdej instancji, aby go znaleźć (co jest prawdopodobnie gorsze ze względu na pośrednie), lub możesz znaleźć sposób, aby zapisać go raz dla każdego wspólnego typu dzielenia wartości z tą regułą - - np. pole statyczne na typie domeny lub równoważnej metodzie, jak powiedział IMSoP.ZipCode
Abstrakcji mogłoby mieć sens tylko wtedy, gdyAddress
klasa nie mieć równieżTownName
właściwość. W przeciwnym razie masz pół abstrakcji: kod pocztowy oznacza miasto, ale te dwa powiązane fragmenty informacji znajdują się w różnych klasach. To nie ma sensu.Jednak nawet wtedy nie jest to prawidłowe zastosowanie (a raczej rozwiązanie) prymitywnej obsesji; który, jak rozumiem, koncentruje się głównie na dwóch rzeczach:
Twoja sprawa nie jest żadna. Adres to dobrze zdefiniowana koncepcja z wyraźnie niezbędnymi właściwościami (ulica, numer, kod pocztowy, miasto, stan, kraj, ...). Nie ma żadnego powodu do rozbicia tych danych, ponieważ ma on jedną odpowiedzialność: wyznaczenie lokalizacji na Ziemi. Adres wymaga wszystkich tych pól, aby był znaczący. Połowa adresu jest bezcelowa.
W ten sposób wiesz, że nie musisz dalej dzielić: dalsze rozbicie go zniszczyłoby funkcjonalną intencję
Address
klasy. Podobnie, nie potrzebujeszName
podklasy do użycia wPerson
klasie, chyba żeName
(bez osoby związanej) jest sensownym pojęciem w Twojej domenie. Które to (zwykle) nie jest. Nazwiska służą do identyfikacji osób, zwykle same nie mają żadnej wartości.źródło
Name
podklasy do użycia wPerson
klasie, chyba że Nazwa (bez osoby związanej) jest znaczącym pojęciem w Twojej domenie ”. Gdy masz niestandardową weryfikację nazw, nazwa staje się znaczącym pojęciem w Twojej domenie; który wyraźnie wspomniałem jako prawidłowy przypadek użycia dla użycia podtypu. Po drugie, do sprawdzania poprawności kodu pocztowego wprowadzasz dodatkowe założenia, takie jak kody pocztowe, które muszą być zgodne z formatem danego kraju. Poruszasz temat znacznie szerszy niż cel pytania OP.Z artykułu:
Kod źródłowy jest odczytywany znacznie częściej niż jest zapisywany. Tak więc problem jo-jo, polegający na konieczności przełączania się między wieloma plikami jest problemem.
Jednak nie , problem jo-jo wydaje się o wiele bardziej istotny, gdy mamy do czynienia z głęboko zależnymi modułami lub klasami (które wzywają się do siebie nawzajem). To szczególny rodzaj koszmaru do przeczytania i prawdopodobnie to, co miał na myśli twórca problemu jo-jo.
Jednak - tak , ważne jest unikanie zbyt wielu warstw abstrakcji!
Na przykład nie zgadzam się z założeniem poczynionym w odpowiedzi mmmaaa, że „ nie trzeba jo-jo, aby [(odwiedzić)] klasę ZipCode, aby zrozumieć klasę adresu”. Z mojego doświadczenia wynika, że to robisz - przynajmniej za pierwszym razem, gdy czytasz kod. Jednak, jak inni zauważyli, że są chwile, kiedy
ZipCode
klasa jest właściwe.YAGNI (Ya Ain't Gonna Need It) to lepszy wzorzec do naśladowania, aby uniknąć kodu Lasagna (kod ze zbyt wieloma warstwami) - abstrakcje, takie jak typy i klasy, są pomocne dla programisty i nie powinny być używane, chyba że są pomoc.
Osobiście staram się „zapisywać wiersze kodu” (i oczywiście powiązane „zapisywanie plików / modułów / klas” itp.). Jestem pewien, że są tacy, którzy zastosowaliby do mnie epitet „prymitywnej obsesji” - uważam, że ważniejsze jest posiadanie kodu, który można łatwo zrozumieć, niż martwić się etykietami, wzorami i anty-wzorami. Właściwy wybór, kiedy należy utworzyć funkcję, moduł / plik / klasę lub umieścić funkcję we wspólnej lokalizacji, jest bardzo sytuacyjny. Z grubsza celuję w 3-100 funkcji liniowych, 80-500 plików liniowych i „1, 2, n” w przypadku kodu biblioteki wielokrotnego użytku ( SLOC - bez komentarzy i schematu; zazwyczaj chcę co najmniej 1 dodatkowy SLOC minimum na linię obowiązkową płyta grzewcza).
Większość pozytywnych wzorców powstało od deweloperów, którzy robili to dokładnie wtedy , gdy ich potrzebowali . O wiele ważniejsze jest nauczenie się pisania czytelnego kodu niż próba zastosowania wzorców bez rozwiązania tego samego problemu. Każdy dobry programista może wdrożyć wzór fabryczny, nie widząc go wcześniej w rzadkim przypadku, gdy jest on odpowiedni dla ich problemu. Użyłem wzorca fabrycznego, wzorca obserwatora i prawdopodobnie setek innych, nie znając ich nazwy (tzn. Czy istnieje „wzorzec przypisania zmiennej”?). Dla zabawnego eksperymentu - zobacz, ile wzorców GoF jest wbudowanych w język JS - przestałem liczyć po około 12-15 w 2009 roku. Wzorzec Factory jest tak prosty, jak na przykład zwracanie obiektu z konstruktora JS - nie trzeba WidgetFactory.
Więc - tak , czasami
ZipCode
jest to dobra klasa. Jednak nie , problem jo-jo nie jest ściśle istotny.źródło
Problem jo-jo jest istotny tylko wtedy, gdy trzeba przewracać w przód iw tył. Jest to spowodowane jedną lub dwiema rzeczami (czasami obie):
Jeśli możesz spojrzeć na nazwę i mieć rozsądne pojęcie o tym, co ona robi, a metody i właściwości robią racjonalnie oczywiste rzeczy, nie musisz patrzeć na kod, możesz go po prostu użyć. Taki jest przede wszystkim cel klas - są to modułowe fragmenty kodu, których można używać i rozwijać w izolacji. Jeśli musisz spojrzeć na coś innego niż interfejs API, aby zobaczyć, co robi klasa, jest to w najlepszym wypadku częściowa awaria.
źródło
Pamiętaj, że nie ma srebrnej kuli. Jeśli piszesz niezwykle prostą aplikację, którą należy szybko przeszukać, prosty ciąg znaków może to zrobić. Jednak w 98% przypadków obiekt Value opisany przez Erica Evansa w DDD byłby idealnie dopasowany. Możesz łatwo zobaczyć wszystkie korzyści, jakie zapewniają obiekty wartości, czytając.
źródło