Nigdy nie używaj ciągów w Javie? [Zamknięte]


73

Natknąłem się na wpis na blogu, który zniechęca do korzystania z napisów w Javie do powodowania braku semantyki w kodzie, sugerując, aby zamiast tego używać cienkich klas opakowań. Oto przykłady przed i po wspomnianym wpisie ilustrującym tę kwestię:

public void bookTicket(
  String name,
  String firstName,
  String film,
  int count,
  String cinema);

public void bookTicket(
  Name name,
  FirstName firstName,
  Film film,
  Count count,
  Cinema cinema);

Z mojego doświadczenia związanego z czytaniem blogów programistycznych doszedłem do wniosku, że 90% to nonsens, ale zastanawiam się, czy to słuszny punkt. Jakoś nie wydaje mi się to właściwe, ale nie mogłem dokładnie określić, co jest nie tak ze stylem programowania.


Odpowiednia dyskusja na oryginalnej Wiki: „co to jest toString ()?”
Christoffer Hammarström

5
Och, wow, ten wpis na blogu sprawił, że poczułem się fizycznie chory
Rob

5
Przeczytałem co najmniej jedną książkę (która mogła być pierwszą edycją Code Complete), która zaleciła wpisanie wszystkich twoich prymitywnych typów, aby miały nazwy z domeny problemu. Ten sam pomysł.
user16764

9
stwierdzenie zaczynające się od „nigdy” jest „zawsze” fałszywe!
Florents Tselai

1
Ten facet jest CTO ?!
Hyangelo

Odpowiedzi:


88

Hermetyzacja służy ochronie twojego programu przed zmianami . Czy przedstawienie nazwy zmieni się? Jeśli nie, to marnujesz swój czas i obowiązuje YAGNI.

Edycja: Przeczytałem post na blogu i ma on zasadniczo dobry pomysł. Problem polega na tym, że przeskoczył to za daleko. Coś takiego String orderId jest naprawdę złe, ponieważ prawdopodobnie "!"£$%^&*())ADAFVFnie jest ważne orderId. Oznacza to, że Stringreprezentuje o wiele więcej możliwych wartości niż są prawidłowe orderIds. Jednak w przypadku czegoś takiego jak namea nie można przewidzieć, która nazwa może być poprawna, a która nie Stringjest poprawna name.

Po pierwsze (poprawnie) redukujesz możliwe dane wejściowe tylko do poprawnych. W drugim przypadku nie udało się zawęzić możliwych prawidłowych danych wejściowych.

Edytuj ponownie: Rozważ przypadek nieprawidłowego wprowadzania danych. Jeśli napiszesz „Gareth Gobulcoque” jako swoje imię, będzie to wyglądać głupio, nie będzie końca świata. Jeśli wpiszesz niepoprawny OrderID, istnieje szansa, że ​​po prostu nie zadziała.


5
W przypadku identyfikatorów zamówień prawdopodobnie wolałbym dodać zaznaczenie w kodzie akceptującym identyfikatory i pozostawiającym ciąg znaków. Klasy powinny zapewniać metody działania z danymi. Jeśli jakaś klasa sprawdza tylko poprawność niektórych danych, a następnie nic nie robi, nie wydaje mi się to właściwe. OOP jest dobre, ale nie powinieneś przesadzać.
Malcolm,

13
@Malcolm: Ale wtedy nie możesz wiedzieć, które ciągi są sprawdzane, chyba że sprawdzasz je raz za razem.
DeadMG

7
„[...] nie możesz przewidzieć, która nazwa może być poprawna, a dowolny Łańcuch jest prawidłową nazwą”. Myślę, że jedną z zalet tej techniki jest to, że jeśli twój parametr jest typu Name, nie możesz przypadkowo przekazać innej niepowiązanej wartości ciągu. Tam, gdzie oczekujesz Name, Nameskompiluje się tylko testament, a Ciąg „Jestem ci winien 5 $” nie może zostać przypadkowo zaakceptowany. (Uwaga: nie ma to związku z żadnym sprawdzaniem poprawności nazw!)
Andres F.

6
Tworzenie typów specyficznych dla konkretnego zastosowania dodaje bogactwo semantyczne i pomaga zwiększyć bezpieczeństwo. Co więcej, tworzenie typów reprezentujących określone wartości bardzo pomaga, gdy używasz kontenera IoC do automatycznego tworzenia klas - łatwo rozpoznać właściwą fasolę do użycia, gdy jest to jedyna fasola zarejestrowana dla konkretnej klasy. Wymagany jest znacznie większy wysiłek, gdy jest to tylko jeden z wielu zarejestrowanych ciągów.
Dathan

3
@DeadMG To argumentacja, a nie coś, co możemy rozwiązać w komentarzach. Powiedziałbym, że zdarzają się przypadki niewłaściwego umieszczania wartości typów pierwotnych, a utwardzanie interfejsów to jeden ze sposobów na poprawę sytuacji.
Andres F.

46

To po prostu szalone :)


2
To także moja opinia, ale chodzi o to, że pamiętam tę sugestię w „Code Complete”. Oczywiście nie wszystko w tej książce jest niepodważalne, ale przynajmniej każe mi się zastanowić dwa razy, zanim odrzuci pomysł.
DPM

8
Wspomniałeś o niektórych hasłach bez żadnego uzasadnienia. Czy możesz rozwinąć swoją opinię? Na przykład niektóre zaakceptowane wzorce i funkcje językowe mogą wyglądać na dodatkową złożoność, ale oferują coś cennego w zamian (na przykład: pisanie statyczne)
Andres F.

12
Zdrowy rozsądek nie jest powszechny.
Kaz Dragon

1
+1, do tego dodałbym, że gdy język obsługuje nazwane parametry, a IDE jest dobry, tak jest w przypadku C # i VS2010, wówczas nie trzeba kompensować braku funkcji w języku szalonymi wzorami . Nie ma potrzeby, aby klasa o nazwie X i klasa o nazwie Y, jeśli można pisać. var p = new Point(x: 10, y:20);Nie jest tak, że Cinema różni się od łańcucha. Zrozumiałbym, gdybyśmy mieli do czynienia z wielkościami fizycznymi, takimi jak ciśnienie, temperatura, energia, gdzie jednostki różnią się, a niektóre nie mogą być ujemne. Autor bloga musi wypróbować funkcjonalność.
Job

1
+1 za „Nie przesadzaj!”
Ivan

23

W większości zgadzam się z autorem. Jeśli istnieje jakieś zachowanie właściwe dla pola, takie jak sprawdzanie poprawności identyfikatora zamówienia, wówczas utworzyłbym klasę reprezentującą ten typ. Jego druga uwaga jest jeszcze ważniejsza: jeśli masz zestaw pól reprezentujących pewne pojęcia, takie jak adres, utwórz klasę dla tego pojęcia. Jeśli programujesz w Javie, płacisz wysoką cenę za pisanie statyczne. Równie dobrze możesz uzyskać całą wartość, jaką możesz.


2
Czy downvoter może komentować?
kevin cline

Jaką wysoką cenę płacimy za pisanie statyczne?
Richard Tingle,

@RichardTingle: czas ponownej kompilacji kodu i ponownego uruchomienia maszyny JVM dla każdej zmiany kodu. Czas stracony po wprowadzeniu zmiany zgodnej ze źródłami, ale niezgodnej binarnie i rzeczy, które wymagają ponownej kompilacji, nie są uwzględniane i pojawia się „MissingMethodException”.
kevin cline,

Nigdy nie spotkałem się z problemami z aplikacjami Java (co przy ciągłej kompilacji jest zwykle kompilowane do czasu uruchomienia), ale jest to słuszna kwestia w przypadku WAR WAR, dla których ponowne wdrożenie wydaje się nieco wolniejsze
Richard Tingle

16

Nie rób tego; to nadmiernie skomplikuje rzeczy, a Ty nie będziesz tego potrzebował

... to odpowiedź, którą napisałbym tutaj 2 lata temu. Teraz jednak nie jestem tego taki pewien; w rzeczywistości w ostatnich miesiącach zacząłem migrować stary kod do tego formatu, nie dlatego, że nie mam nic lepszego do roboty, ale dlatego, że naprawdę potrzebowałem go do wdrożenia nowych funkcji lub zmiany istniejących. Rozumiem automatyczne awersje, które inni widzą w tym kodzie, ale myślę, że jest to coś, co zasługuje na poważne przemyślenie.


Korzyści

Najważniejszą zaletą jest możliwość modyfikacji i rozszerzenia kodu. Jeśli użyjesz

class Point {
    int x,y;
    // other point operations
}

zamiast po prostu przekazać kilka liczb całkowitych - co jest niestety w wielu interfejsach - znacznie łatwiej jest później dodać inny wymiar. Lub zmień typ na double. Jeśli użyjesz List<Author> authorslub List<Person> authorszamiast List<String> authorstego później znacznie łatwiej będzie dodać więcej informacji do tego, co reprezentuje autor. Zapisując to w ten sposób, wydaje mi się, że stwierdzam to, co oczywiste, ale w praktyce byłem winny wielokrotnego używania ciągów w ten sposób, szczególnie w przypadkach, gdy na początku nie było to oczywiste, potrzebowałem więcej niż sznurek.

Obecnie próbuję refaktoryzować listę ciągów, która jest przeplatana w całym kodzie, ponieważ potrzebuję tam więcej informacji i odczuwam ból: \

Poza tym zgadzam się z autorem bloga, że zawiera on więcej informacji semantycznych , co ułatwia czytelnikowi zrozumienie. Podczas gdy parametry często otrzymują znaczące nazwy i otrzymują specjalną linię dokumentacji, często nie dzieje się tak w przypadku pól lub miejscowych.

Ostatnią korzyścią jest bezpieczeństwo typu , z oczywistych powodów, ale moim zdaniem jest to drobna sprawa.

Wady

Pisanie zajmuje więcej czasu . Pisanie małej klasy jest szybkie i łatwe, ale nie wymaga wysiłku, szczególnie jeśli potrzebujesz wielu takich klas. Jeśli przestajesz pisać co 3 minuty, aby napisać nową klasę opakowań, może to być również poważna szkoda dla koncentracji. Chciałbym jednak pomyśleć, że taki stan wysiłku zwykle występuje tylko na pierwszym etapie pisania dowolnego fragmentu kodu; Zwykle mogę szybko uzyskać całkiem niezły pomysł na to, jakie podmioty będą musiały być zaangażowane.

Może obejmować wiele zbędnych seterów (lub konstrukcji) i getterów . Autor bloga podaje naprawdę brzydki przykład new Point(x(10), y(10))zamiast new Point(10, 10), i chciałbym dodać, że użycie może również obejmować rzeczy takie jak Math.max(p.x.get(), p.y.get())zamiast Math.max(p.x, p.y). Długi kod jest często uważany za trudniejszy do odczytania i słusznie. Ale szczerze mówiąc, mam wrażenie, że wiele kodu przenosi obiekty i tylko wybrane metody go tworzą, a jeszcze mniej potrzebuje dostępu do jego drobnych szczegółów (co zresztą nie jest OOPy).

Sporny

Powiedziałbym, czy to pomaga w czytelności kodu jest dyskusyjne. Tak, więcej informacji semantycznych, ale dłuższy kod. Tak, łatwiej jest zrozumieć rolę każdego lokalnego, ale trudniej jest zrozumieć, co możesz z nim zrobić, chyba że przeczytasz jego dokumentację.


Podobnie jak w przypadku większości innych szkół programistycznych, myślę, że niezdrowe jest doprowadzenie tego do skrajności. Nie widzę, aby kiedykolwiek oddzielałem współrzędną xiy, aby każdy był innego typu. Nie uważam za Countkonieczne, kiedy intpowinno wystarczyć. Nie podoba mi się unsigned intużycie w C - teoretycznie dobre, ale po prostu nie zapewnia wystarczającej ilości informacji i zabrania późniejszego rozszerzania kodu w celu obsługi tego magicznego -1. Czasami potrzebujesz prostoty.

Myślę, że ten post na blogu jest nieco skrajny. Ale ogólnie nauczyłem się z bolesnego doświadczenia, że ​​podstawową ideą tego są właściwe rzeczy.

Mam głęboką awersję do przerobionego kodu. Naprawdę. Ale dobrze wykorzystane, nie sądzę, aby ta nadmierna inżynieria.


5

Chociaż jest to rodzaj przesady, często myślę, że większość rzeczy, które widziałem, jest niedostatecznie opracowanych.

To nie tylko „bezpieczeństwo”. Jedną z naprawdę fajnych rzeczy w Javie jest to, że bardzo pomaga w zapamiętywaniu / ustalaniu, czego potrzebuje / oczekuje dana metoda biblioteki.

Biblioteka WORST (jak dotąd) Java, z którą pracowałem, została napisana przez kogoś, kto bardzo lubił Smalltalk i modelował bibliotekę GUI, aby działała bardziej jak smalltalk - problem polegał na tym, że każda metoda pobierała ten sam obiekt podstawowy, ale nie był w stanie WYKORZYSTYWAĆ wszystkiego, na co mógłby być rzutowany obiekt podstawowy, więc wróciłeś do zgadywania, co przejść do metod i nie wiedząc, czy zawiodłeś do czasu uruchomienia (Coś, z czym miałem do czynienia za każdym razem, gdy ja pracował w C).

Kolejny problem - jeśli przekazujesz ciągi, int, kolekcje i tablice bez obiektów, wszystko, co masz, to kule danych bez znaczenia. Wydaje się to naturalne, gdy myślisz w kategoriach bibliotek, z których będzie korzystać „jakaś aplikacja”, ale przy projektowaniu całej aplikacji o wiele bardziej pomocne jest przypisanie znaczenia (kodu) do wszystkich danych w miejscu, w którym dane są zdefiniowane, i myślenie tylko w warunki interakcji tych obiektów wysokiego poziomu. Jeśli przekazujesz prymitywy zamiast obiektów, to z definicji zmieniasz dane w innym miejscu niż to, w którym jest zdefiniowane (właśnie dlatego tak naprawdę nie lubię Seterów i Getterów - ta sama koncepcja, operujesz na danych, które nie są twoje).

Wreszcie, jeśli zdefiniujesz osobne obiekty dla wszystkiego, zawsze masz świetne miejsce do sprawdzania wszystkiego - na przykład, jeśli utworzysz obiekt dla kodu pocztowego, a później okaże się, że musisz upewnić się, że kod pocztowy zawsze zawiera 4-cyfrowe rozszerzenie, masz idealne miejsce do tego.

To nie jest zły pomysł. Myśląc o tym, nie jestem nawet pewien, czy powiedziałbym, że był w ogóle przeprojektowany, po prostu łatwiej jest pracować z nim pod każdym względem - jedynym wyjątkiem jest mnożenie małych klas, ale klasy Java są tak lekkie i łatwe napisać, że nie jest to nawet koszt (można je nawet wygenerować).

Byłbym bardzo zainteresowany, aby zobaczyć dobrze napisany projekt Java, w którym zdefiniowano zbyt wiele klas (tam, gdzie utrudniało to programowanie), zaczynam myśleć, że nie jest możliwe posiadanie zbyt wielu klas.


3

Myślę, że musisz spojrzeć na tę koncepcję z innego punktu wyjścia. Spójrz z perspektywy projektanta bazy danych: typy przekazane w pierwszym przykładzie nie definiują parametrów w unikalny sposób, nie mówiąc już o użyteczny sposób.

public void bookTicket(
  String name,
  String firstName,
  String film,
  int count,
  String cinema);

Potrzebne są dwa parametry, aby określić faktycznego patrona, który rezerwuje bilety, możesz mieć dwa różne filmy o identycznych nazwach (np. Przeróbki), możesz mieć ten sam film o różnych nazwach (np. Tłumaczenia). Pewna sieć kin może mieć różne oddziały, tak jak idziesz do czynienia z tym w ciąg i w spójny sposób (np używasz $chain ($city)lub $chain in $citynawet coś innego i jak idziesz, aby upewnić się, że jest to konsekwentnie stosowane. Najgorsze jest określenie twojego patrona za pomocą dwóch parametrów, fakt, że podane jest zarówno imię, jak i nazwisko, nie gwarantuje ważnego klienta (i nie można rozróżnić dwóch John Doe).

Odpowiedzią na to jest deklarowanie typów, ale rzadko będą to cienkie opakowania, jak pokazałem powyżej. Najprawdopodobniej będą one służyć do przechowywania danych lub będą połączone z pewnego rodzaju bazą danych. Więc Cinemaobiekt prawdopodobnie będzie miał nazwę, lokalizację ... i w ten sposób pozbędziesz się takich dwuznaczności. Jeśli są cienkimi opakowaniami, są przypadkiem.

Tak więc w blogu IMHO jest tylko powiedzenie „upewnij się, że przekazujesz poprawne typy”, jego autor właśnie dokonał zbyt ograniczonego wyboru, aby wybrać w szczególności podstawowe typy danych (co jest złym komunikatem).

Proponowana alternatywa jest lepsza:

public void bookTicket(
  Name name,
  FirstName firstName,
  Film film,
  Count count,
  Cinema cinema);

Z drugiej strony myślę, że post na blogu idzie zbyt daleko, by wszystko opakować. Countjest zbyt ogólny, mógłbym z tym policzyć jabłka lub pomarańcze, dodać je i nadal mieć na rękach sytuację, w której system typów pozwala mi wykonywać bezsensowne operacje. Możesz oczywiście zastosować tę samą logikę, co na blogu i zdefiniować typy CountOfOrangesitp., Ale jest to również głupie.

Za to, co jest warte, napisałbym coś takiego

public Ticket bookTicket(
  Person patron,
  Film film,
  int numberOfTickets,
  Cinema cinema);

Krótko mówiąc: nie powinieneś przekazywać nonsensownych zmiennych; Jedynym momentem, w którym faktycznie określasz obiekt o wartości, która nie określa rzeczywistego obiektu, jest uruchomienie zapytania (np. public Collection<Film> findFilmsWithTitle(String title)) lub przygotowanie dowodu koncepcji. Utrzymuj swój system typów w czystości, więc nie używaj typu, który jest zbyt ogólny (np. Film reprezentowany przez a String) lub zbyt restrykcyjny / specyficzny / wymyślony (np. CountZamiast int). Użyj typu, który definiuje Twój obiekt w sposób unikalny i jednoznaczny, gdy tylko jest to możliwe i wykonalne.

edycja : jeszcze krótsze podsumowanie. W przypadku małych aplikacji (np. Weryfikacja koncepcji): po co zawracać sobie głowę skomplikowanym projektem? Po prostu użyj Stringlub inti kontynuuj.

W przypadku dużych aplikacji: czy naprawdę jest tak prawdopodobne, że masz wiele klas, które składają się z jednego pola o podstawowym typie danych? Jeśli masz niewiele takich klas, po prostu masz „normalne” przedmioty, nic specjalnego się tam nie dzieje.

Uważam, że pomysł na enkapsulację łańcuchów ... to po prostu projekt, który jest niekompletny: zbyt skomplikowany dla małych aplikacji, niewystarczająco kompletny dla dużych aplikacji.


Rozumiem twój punkt widzenia, ale twierdzę, że zakładasz więcej niż twierdzi autor. Z tego, co wiemy, jego model ma tylko Sznurek dla patrona, Sznurek do filmu i tak dalej. Ta hipotetyczna dodatkowa funkcjonalność jest w istocie sednem problemu, więc albo był bardzo „roztargniony”, aby pominąć to przy podejmowaniu swojej decyzji, albo uważa, że ​​powinniśmy zapewnić większą moc semantyczną tylko dlatego. Ponownie, o ile wiemy, to ten drugi miał na myśli.
DPM

@Jubbat: w rzeczy samej zakładam więcej niż to, co twierdzi autor. Ale chodzi mi o to, że albo masz prostą aplikację, w którym to przypadku jest zbyt skomplikowana. W tej skali łatwość konserwacji nie jest problemem, a rozróżnienie semantyczne ogranicza prędkość kodowania. Z drugiej strony, jeśli twoja aplikacja jest duża, warto odpowiednio zdefiniować swoje typy (ale jest mało prawdopodobne, aby były to proste opakowania). IMHO, jego przykłady po prostu nie są przekonujące lub mają poważne wady projektowe poza tym, co próbuje zrobić.
Egon

2

Dla mnie to robi to samo, co przy użyciu regionów w C #. Ogólnie rzecz biorąc, jeśli uważasz, że jest to konieczne, aby twój kod był czytelny, masz większe problemy, na które powinieneś poświęcić swój czas.


2
+1 za sugerowanie leczenia objawów, a nie przyczyny.
Job

2

Powiedziałbym, że to naprawdę dobry pomysł w języku z silnie typowaną czcionką.

W Javie tego nie masz, więc utworzenie zupełnie nowej klasy dla tych rzeczy oznacza, że ​​koszt prawdopodobnie przewyższa korzyść. Możesz także uzyskać 80% korzyści, zachowując ostrożność przy nazywaniu zmiennych / parametrów.


0

Byłoby dobrze, JEŻELI Łańcuch (koniec liczby całkowitej i ... mówiąc tylko o łańcuchu) nie był końcowy, więc te klasy mogłyby być pewnym (ograniczonym) łańcuchem o znaczeniu i nadal mogą być wysyłane do jakiegoś niezależnego obiektu, który wie, jak sobie z tym poradzić typ podstawowy (bez konwersacji tam iz powrotem).

A „dobroci” tego rosną, gdy są np. ograniczenia dotyczące wszystkich nazwisk.

Ale podczas tworzenia aplikacji (nie biblioteki) zawsze można ją refaktoryzować. Więc staram się zacząć bez niego.


0

Na przykład: w naszym obecnie opracowanym systemie istnieje wiele różnych podmiotów, które można zidentyfikować za pomocą różnego rodzaju identyfikatorów (ze względu na wykorzystanie systemów zewnętrznych), czasami nawet tego samego rodzaju. Wszystkie identyfikatory są ciągami - więc jeśli ktoś pomyli, jaki identyfikator powinien zostać przekazany jako parametr, nie pojawia się błąd czasu kompilacji, ale program wysadzi się w czasie wykonywania. Zdarza się to dość często. Muszę więc powiedzieć, że podstawowym celem tej zasady nie jest ochrona przed zmianami (choć służy to również), ale ochrona się przed błędami. W każdym razie, jeśli ktoś projektuje API, musi odzwierciedlać koncepcje domeny, więc koncepcyjnie przydatne jest definiowanie klas specyficznych dla domeny - wszystko zależy od tego, czy w programistach panuje porządek,


@downvoter: czy możesz podać wyjaśnienie?
thSoft
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.