Ostatnio miałem problem z czytelnością mojego kodu.
Miałem funkcję, która wykonała operację i zwróciła ciąg reprezentujący identyfikator tej operacji do przyszłego odwołania (trochę jak OpenFile w Windows zwracający uchwyt). Użytkownik użyje tego identyfikatora później, aby rozpocząć operację i monitorować jej zakończenie.
Identyfikator musiał być ciągiem losowym ze względu na obawy związane z interoperacyjnością. Stworzyło to metodę, która miała bardzo niejasny podpis:
public string CreateNewThing()
To sprawia, że zamiar typu zwracanego jest niejasny. Pomyślałem, że mogę zawinąć ten ciąg w inny typ, który uczyni jego znaczenie bardziej zrozumiałym:
public OperationIdentifier CreateNewThing()
Typ będzie zawierał tylko ciąg znaków i będzie używany za każdym razem, gdy zostanie użyty.
Oczywistym jest, że zaletą tego sposobu działania jest większe bezpieczeństwo typu i wyraźniejsze zamiary, ale tworzy także znacznie więcej kodu i kodu, który nie jest bardzo idiomatyczny. Z jednej strony podoba mi się dodatkowe bezpieczeństwo, ale także tworzy dużo bałaganu.
Czy uważasz, że ze względów bezpieczeństwa dobrą praktyką jest owijanie prostych typów w klasę?
źródło
Odpowiedzi:
Prymitywy, takie jak
string
lubint
, nie mają znaczenia w domenie biznesowej. Bezpośrednią konsekwencją tego jest to, że możesz błędnie użyć adresu URL, gdy oczekiwany jest identyfikator produktu, lub użyć ilości, gdy spodziewasz się ceny .Z tego powodu wyzwanie Kalistenika obiektów obejmuje zawijanie prymitywów jako jedną z jego zasad:
Ten sam dokument wyjaśnia, że istnieje dodatkowa korzyść:
Rzeczywiście, gdy stosowane są prymitywy, zazwyczaj niezwykle trudne jest śledzenie dokładnej lokalizacji kodu związanego z tymi typami, co często prowadzi do poważnego powielania kodu . Jeśli jest
Price: Money
klasa, naturalne jest sprawdzenie zasięgu w środku. Jeżeli zamiast tego do przechowywania cen produktów stosuje sięint
(gorzej, adouble
), kto powinien potwierdzić zakres? Produkt? Rabat? Wózek?Wreszcie trzecią korzyścią niewymienioną w dokumencie jest możliwość stosunkowo łatwej zmiany rodzaju bazowego. Jeśli dzisiaj mój
ProductId
mashort
typ podstawowy, a później muszę go użyćint
, istnieje szansa, że kod do zmiany nie obejmie całej bazy kodu.Wadą - i ten sam argument stosuje się do każdej reguły ćwiczenia Calisthenics Object - jest to, że jeśli szybko staje się zbyt przytłaczający, aby stworzyć klasę dla wszystkiego . Jeśli
Product
zawiera,ProductPrice
które dziedziczy, zPositivePrice
którego dziedziczy, zPrice
którego dziedziczyMoney
, to nie jest to czysta architektura, ale raczej kompletny bałagan, w którym aby znaleźć jedną rzecz, opiekun powinien otwierać kilkadziesiąt plików za każdym razem.Inną kwestią do rozważenia jest koszt (pod względem linii kodu) tworzenia dodatkowych klas. Jeśli opakowania są niezmienne (jak zwykle powinny), oznacza to, że jeśli weźmiemy C #, musisz mieć w opakowaniu co najmniej:
ToString()
,Equals
iGetHashCode
zastępuje (również dużo LOC).i ewentualnie, w stosownych przypadkach:
==
i!=
operatorów,Sto LOC na proste opakowanie sprawia, że jest on zbyt wygórowany, dlatego też możesz być całkowicie pewien długoterminowej rentowności takiego opakowania. Pojęcie zakresu wyjaśnione przez Thomasa Junk jest tutaj szczególnie istotne. Pisanie setek LOC-ów, które będą reprezentować
ProductId
używane w całej bazie kodu, wygląda całkiem użytecznie. Napisanie klasy tego rozmiaru dla fragmentu kodu, który tworzy trzy wiersze w ramach jednej metody, jest znacznie bardziej wątpliwe.Wniosek:
Zawijaj operacje podstawowe w klasach, które mają znaczenie w domenie biznesowej aplikacji, gdy (1) pomaga zmniejszyć liczbę błędów, (2) zmniejsza ryzyko duplikacji kodu lub (3) pomaga później zmienić typ bazowy.
Nie zawijaj automatycznie wszystkich prymitywów, które znajdziesz w kodzie: w wielu przypadkach użycie
string
lubint
jest całkowicie w porządku.W praktyce
public string CreateNewThing()
zwracanie instancjiThingId
klasy zamiaststring
może pomóc, ale możesz również:Zwraca instancję
Id<string>
klasy, czyli obiekt typu ogólnego wskazujący, że typem bazowym jest łańcuch. Zaletą czytelności jest brak konieczności utrzymywania wielu typów.Zwraca instancję
Thing
klasy. Jeśli użytkownik potrzebuje tylko identyfikatora, można to łatwo zrobić za pomocą:źródło
Użyłbym zakresu jako ogólnej zasady: im węższy zakres generowania i konsumowania
values
, tym mniejsze prawdopodobieństwo, że będziesz musiał stworzyć obiekt reprezentujący tę wartość.Powiedz, że masz następujący pseudokod
wtedy zakres jest bardzo ograniczony i nie widziałbym sensu w tworzeniu tego typu. Ale powiedzmy, że generujesz tę wartość w jednej warstwie i przekazujesz ją do kolejnej warstwy (lub nawet innego obiektu), wtedy stworzenie takiego typu byłoby idealnie uzasadnione.
źródło
doFancyOtherstuff()
podprogramu, możesz pomyśleć, żejob.do(id)
odwołanie nie jest wystarczająco lokalne, aby można je było przechowywać jako prosty ciąg.Systemy typu statycznego mają na celu zapobieganie nieprawidłowemu wykorzystaniu danych.
Istnieją oczywiste przykłady typów, które to robią:
Istnieją bardziej subtelne przykłady
Możemy mieć ochotę użyć
double
zarówno ceny, jak i długości, lub użyćstring
zarówno nazwy, jak i adresu URL. Ale zrobienie tego psuje nasz niesamowity system typów i pozwala tym nadużyciom przejść testy statyczne języka.Pomylenie funta sekund z Newtonem sekundami może mieć złe wyniki w czasie wykonywania .
Jest to szczególnie problem z łańcuchami. Często stają się one „uniwersalnym typem danych”.
Jesteśmy przyzwyczajeni do interfejsów tekstowych głównie z komputerami, a my często rozszerzyć te ludzkie interfejsy (UIS) do interfejsów programistycznych (API). Myślimy o 34,25 jako o znakach 34,25 . Myślimy o randce jak o postaciach 05-03-2015 . UUID uważamy za znaki 75e945ee-f1e9-11e4-b9b2-1697f925ec7b .
Ale ten model mentalny szkodzi abstrakcji API.
Podobnie reprezentacje tekstowe nie powinny odgrywać żadnej roli w projektowaniu typów i interfejsów API. Uważaj na
string
! (i inne nazbyt ogólne „prymitywne” typy)Typy komunikują „jakie operacje mają sens”.
Na przykład kiedyś pracowałem na kliencie z interfejsem API REST HTTP. REST, poprawnie wykonane, używaj encji hipermedialnych, które mają hiperłącza wskazujące na powiązane encje. W tym kliencie wpisano nie tylko podmioty (np. Użytkownik, Konto, Subskrypcja), ale również linki do tych podmiotów (UserLink, AccountLink, SubscriptionLink). Linki były niewiele więcej niż opakowaniami
Uri
, ale osobne typy uniemożliwiały próbę użycia AccountLink do pobrania użytkownika. Gdyby wszystko było jasneUri
- lub nawet gorzejstring
- błędy te można było znaleźć tylko w czasie wykonywania.Podobnie w twojej sytuacji masz dane, które są wykorzystywane tylko w jednym celu: do identyfikacji
Operation
. Nie należy go używać do niczego innego i nie powinniśmy próbować identyfikowaćOperation
s z losowymi ciągami, które stworzyliśmy. Utworzenie osobnej klasy zwiększa czytelność i bezpieczeństwo kodu.Oczywiście wszystkie dobre rzeczy można wykorzystać w nadmiarze. Rozważać
ile przejrzystości dodaje do twojego kodu
jak często jest używany
Jeśli „typ” danych (w sensie abstrakcyjnym) jest często używany, do różnych celów i między interfejsami kodu, jest bardzo dobrym kandydatem do bycia odrębną klasą, kosztem gadatliwości.
źródło
05-03-2015
oznacza interpretacja jako data ?Czasami.
Jest to jeden z tych przypadków, w których trzeba rozważyć problemy, które mogą wyniknąć z użycia
string
zamiast bardziej konkretnegoOperationIdentifier
. Jakie są ich nasilenie? Jakie jest ich prawdopodobieństwo?Następnie musisz wziąć pod uwagę koszt korzystania z innego rodzaju. Jak bolesne jest stosowanie? Ile to pracy?
W niektórych przypadkach zaoszczędzisz czas i wysiłek, mając fajny konkretny typ do pracy. W innych nie będzie to warte kłopotu.
Ogólnie rzecz biorąc, uważam, że tego rodzaju rzeczy należy robić więcej niż dzisiaj. Jeśli masz podmiot, który coś znaczy w Twojej domenie, dobrze jest mieć go jako swój własny typ, ponieważ istnieje większe prawdopodobieństwo, że podmiot ten zmieni się / rozwinie wraz z firmą.
źródło
Ogólnie zgadzam się, że wiele razy powinieneś utworzyć typ dla prymitywów i łańcuchów, ale ponieważ powyższe odpowiedzi zalecają utworzenie typu w większości przypadków, wymienię kilka powodów, dla których / kiedy nie:
źródło
short
(na przykład) ma taki sam koszt zawinięty lub rozpakowany. (?)Nie, nie powinieneś definiować typów (klas) dla „wszystkiego”.
Ale, jak podają inne odpowiedzi, często jest to przydatne. Powinieneś rozwijać - świadomie, jeśli to możliwe - uczucie zbytniego tarcia z powodu braku odpowiedniego typu lub klasy podczas pisania, testowania i utrzymywania swojego kodu. Dla mnie początek zbyt dużego tarcia występuje, gdy chcę skonsolidować wiele prymitywnych wartości jako pojedynczą wartość lub gdy muszę zweryfikować wartości (tj. Określić, która ze wszystkich możliwych wartości typu pierwotnego odpowiada prawidłowym wartościom „typ domyślny”).
Przekonałem się, że takie kwestie, które postawiłeś w swoim pytaniu, są odpowiedzialne za zbyt dużo projektowania w moim kodzie. Mam nawyk celowego unikania pisania więcej kodu niż to konieczne. Mamy nadzieję, że piszesz dobre (zautomatyzowane) testy dla swojego kodu - jeśli tak, to możesz z łatwością refaktoryzować kod i dodawać typy lub klasy, jeśli zapewnia to korzyści netto dla ciągłego rozwoju i utrzymania kodu .
Zarówno odpowiedź Telastyna, jak i odpowiedź Thomasa Junk'a bardzo dobrze podkreślają kontekst i użycie odpowiedniego kodu. Jeśli używasz wartości w jednym bloku kodu (np. Metoda, pętla,
using
blok), możesz po prostu użyć prostego typu. Można nawet używać prymitywnego typu, jeśli używasz zestawu wartości wielokrotnie i w wielu innych miejscach. Ale im częściej i szerzej używasz zestawu wartości, a im rzadziej ten zestaw wartości odpowiada wartościom reprezentowanym przez typ pierwotny, tym bardziej powinieneś rozważyć kapsułkowanie wartości w klasie lub typie.źródło
Na powierzchni wygląda na to, że wszystko, co musisz zrobić, to zidentyfikować operację.
Dodatkowo mówisz, co ma zrobić operacja:
Mówisz to w taki sposób, jakby to było „tak, jak używana jest identyfikacja”, ale powiedziałbym, że są to właściwości opisujące operację. To brzmi jak definicja typu, istnieje nawet wzorzec zwany „wzorcem poleceń”, który bardzo dobrze pasuje. .
To mówi
Myślę, że jest to bardzo podobne w porównaniu z tym, co chcesz zrobić ze swoimi operacjami. (Porównaj wyrażenia, które pogrubiłem w obu cudzysłowach) Zamiast zwracać ciąg, zwróć identyfikator
Operation
w sensie abstrakcyjnym, na przykład wskaźnik do obiektu tej klasy.Odnośnie komentarza
Nie, nie zrobiłby tego. Pamiętaj, że wzory są bardzo abstrakcyjne , tak abstrakcyjne, że są w pewnym sensie meta. Oznacza to, że często abstrahują one od samego programowania, a nie jakiejś prawdziwej koncepcji świata. Wzorzec poleceń jest abstrakcją wywołania funkcji. (lub metoda) Jakby ktoś wcisnął przycisk pauzy zaraz po przekazaniu wartości parametrów i tuż przed wykonaniem, aby wznowić później.
Poniżej rozważa się oop, ale motywacja za nim powinna być prawdziwa dla każdego paradygmatu. Chciałbym podać kilka powodów, dla których umieszczenie logiki w poleceniu można uznać za coś złego.
Podsumowując: wzorzec poleceń pozwala na zawinięcie funkcji w obiekt, który zostanie wykonany później. Ze względu na modułowość (funkcjonalność istnieje bez względu na to, czy jest wykonywana za pomocą komendy, czy nie), testowalność (funkcjonalność powinna być testowalna bez komendy) i wszystkie inne brzęczące słowa, które zasadniczo wyrażają potrzebę napisania dobrego kodu, nie wprowadzilibyśmy logiki do polecenia
Ponieważ wzory są abstrakcyjne, może być trudno wymyślić dobre metafory świata rzeczywistego. Oto próba:
„Hej, babciu, czy mógłbyś nacisnąć przycisk nagrywania na telewizorze o godzinie 12, żeby nie przegapić simpsonów na kanale 1?”
Moja babcia nie wie, co się dzieje technicznie, kiedy naciska ten przycisk nagrywania. Logika jest gdzie indziej (w telewizji). I to dobrze. Funkcjonalność jest enkapsulowana, a informacje ukryte przed poleceniem, to użytkownik API, niekoniecznie część logiki ... och jeez znów bełkoczę brzęczące słowa, lepiej dokończę teraz edycję.
źródło
Pomysł zawijania prymitywnych typów,
Oczywiście byłoby to bardzo trudne i niepraktyczne, aby robić to wszędzie, ale ważne jest, aby zawijać typy tam, gdzie jest to wymagane,
Na przykład, jeśli masz klasę Order,
Ważnymi właściwościami do wyszukiwania Zamówienia są głównie OrderId i InvoiceNumber. Kwota i kod waluty są ze sobą ściśle powiązane, a jeśli ktoś zmieni kod waluty bez zmiany kwoty, zamówienie nie będzie już uważane za ważne.
Tak więc tylko zawijanie OrderId, InvoiceNumber i wprowadzenie kompozytu dla waluty ma sens w tym scenariuszu i prawdopodobnie zawijanie opisu nie ma sensu. Tak preferowany wynik może wyglądać,
Nie ma więc powodu, żeby wszystko owijać, ale rzeczy, które naprawdę mają znaczenie.
źródło
Niepopularna opinia:
Zwykle nie powinieneś definiować nowego typu!
Definiowanie nowego typu do zawijania klasy podstawowej lub podstawowej jest czasem nazywane definiowaniem pseudo-typu. IBM opisuje to jako złych praktyk tutaj (koncentrują się w szczególności na nadużycia ze strony generyków w tym przypadku).
Pseudo typy sprawiają, że wspólna funkcja biblioteki jest bezużyteczna.
Funkcje matematyczne Java mogą współpracować ze wszystkimi liczbami pierwotnymi. Ale jeśli zdefiniujesz nową klasę Procent (zawijanie podwójnej liczby, która może mieścić się w przedziale 0 ~ 1), wszystkie te funkcje są bezużyteczne i muszą zostać otoczone przez klasy, które (jeszcze gorzej) muszą wiedzieć o wewnętrznej reprezentacji klasy Procent .
Pseudo typy stają się wirusowe
Podczas tworzenia wielu bibliotek często zdarza się, że te pseudotypy stają się wirusowe. Jeśli użyjesz wyżej wspomnianej klasy procentowej w jednej bibliotece, albo będziesz musiał ją przekonwertować na granicy biblioteki (tracąc wszelkie bezpieczeństwo / znaczenie / logikę / inny powód, dla którego miałeś utworzyć ten typ), albo będziesz musiał stworzyć te klasy dostępne również w innej bibliotece. Zarażanie nowej biblioteki typami, w których wystarczyłoby zwykłe podwójne.
Zabierz wiadomość
Tak długo, jak typ, który zawijasz, nie wymaga dużej logiki biznesowej, odradzałbym pakowanie go w pseudo-klasę. Powinieneś zawinąć klasę tylko wtedy, gdy istnieją poważne ograniczenia biznesowe. W innych przypadkach prawidłowe nazewnictwo zmiennych powinno mieć duży wpływ na przekazywanie znaczenia.
Przykład:
A
uint
może idealnie reprezentować identyfikator użytkownika, możemy nadal używać wbudowanych operatorów Java dlauint
s (takich jak ==) i nie potrzebujemy logiki biznesowej do ochrony „wewnętrznego stanu” identyfikatora użytkownika.źródło
Podobnie jak w przypadku wszystkich wskazówek, umiejętność stosowania zasad jest umiejętnością. Jeśli tworzysz własne typy w języku opartym na typach, sprawdzasz typ. Ogólnie rzecz biorąc, będzie to dobry plan.
Konwencja Naming jest kluczem do czytelności. Oba połączone mogą jasno wyrażać intencje.
Ale /// jest nadal przydatne.
Tak, powiedziałbym, że stwórz wiele własnych typów, gdy ich życie wykracza poza granice klas. Weź również pod uwagę użycie obu
Struct
iClass
nie zawsze klasy.źródło
///
znaczy