Często kodowałem w Pythonie. Teraz, ze względu na pracę, piszę w Javie. Projekty, które wykonuję, są raczej małe i być może Python działałby lepiej, ale istnieją ważne nieinżynieryjne powody, aby używać Java (nie mogę wdawać się w szczegóły).
Składnia Java nie stanowi problemu; to tylko inny język. Ale oprócz składni Java ma kulturę, zestaw metod programistycznych i praktyki uważane za „poprawne”. I na razie zupełnie nie „grokuję” tej kultury. Naprawdę doceniłbym wyjaśnienia lub wskazówki we właściwym kierunku.
Minimalny pełny przykład jest dostępny w pytaniu dotyczącym przepełnienia stosu, które rozpocząłem: https://stackoverflow.com/questions/43619566/returning-a-result-with-several-values-the-java-way/43620339
Mam zadanie - przeanalizować (z jednego ciągu) i obsłużyć zestaw trzech wartości. W Pythonie jest to jednowarstwowy (krotka), w Pascal lub C 5-liniowy rekord / struct.
Zgodnie z odpowiedziami odpowiednik struktury jest dostępny w składni Java, a potrójny jest dostępny w powszechnie używanej bibliotece Apache - jednak „poprawnym” sposobem jest utworzenie oddzielnej klasy dla wartości wraz z pobierający i ustawiający. Ktoś był bardzo miły, aby podać pełny przykład. To było 47 linii kodu (cóż, niektóre z tych linii były puste).
Rozumiem, że ogromna społeczność programistów prawdopodobnie nie jest „zła”. To jest problem z moim zrozumieniem.
Praktyki Pythona optymalizują pod kątem czytelności (co w tej filozofii prowadzi do łatwości konserwacji), a następnie szybkości rozwoju. Praktyki C optymalizują wykorzystanie zasobów. Do czego optymalizują praktyki Java? Moje najlepsze przypuszczenie to skalowalność (wszystko powinno być w stanie gotowym na projekt o milionach LOC), ale jest to bardzo słaby przypuszczenie.
źródło
Odpowiedzi:
Język Java
Uważam, że wszystkie te odpowiedzi nie mają sensu, próbując przypisać zamiar działaniu Java. Gadatliwość Javy nie wynika z tego, że jest zorientowana obiektowo, ponieważ Python i wiele innych języków mają jeszcze składnię terser. Gadatliwość Javy również nie wynika z obsługi modyfikatorów dostępu. Zamiast tego jest to po prostu sposób, w jaki Java została zaprojektowana i ewoluowała.
Java została pierwotnie stworzona jako nieco ulepszone C z OO. Jako taka Java ma składnię z lat 70. Ponadto Java jest bardzo konserwatywna w kwestii dodawania funkcji w celu zachowania kompatybilności wstecznej i umożliwienia jej przetrwania próby czasu. Gdyby Java dodała modne funkcje, takie jak literały XML, w 2005 roku, gdy XML był wściekły, język byłby nadęty z cechami ducha, o które nikt nie dba i które ograniczają jego rozwój 10 lat później. Dlatego w Javie po prostu brakuje dużo nowoczesnej składni, aby precyzyjnie wyrażać pojęcia.
Jednak nic nie stoi na przeszkodzie, aby Java przyjęła tę składnię. Na przykład Java 8 dodała lambdy i odwołania do metod, co znacznie zmniejsza gadatliwość w wielu sytuacjach. Java mogłaby podobnie dodać obsługę deklaracji zwartych typów danych, takich jak klasy przypadków Scali. Ale Java po prostu tego nie zrobiła. Należy pamiętać, że niestandardowe typy wartości są na horyzoncie i ta funkcja może wprowadzić nową składnię do ich deklarowania. Chyba zobaczymy.
Kultura Java
Historia rozwoju Java w przedsiębiorstwie w dużej mierze doprowadziła nas do kultury, którą widzimy dzisiaj. Na przełomie lat 90. i wczesnych XX wieku Java stała się niezwykle popularnym językiem dla aplikacji biznesowych po stronie serwera. W tamtych czasach aplikacje te były w dużej mierze pisane ad-hoc i obejmowały wiele złożonych problemów, takich jak interfejsy API HTTP, bazy danych i przetwarzanie kanałów XML.
W latach dziewięćdziesiątych stało się jasne, że wiele z tych aplikacji ma wiele wspólnego, a ramy do zarządzania tymi problemami, takie jak Hibernacja ORM, parser Xerces XML, JSP i API serwletów oraz EJB, stały się popularne. Jednak chociaż ramy te zmniejszyły wysiłek związany z pracą w konkretnej domenie, którą ustawili do automatyzacji, wymagały konfiguracji i koordynacji. W tamtym czasie, z jakiegokolwiek powodu, popularne było pisanie frameworków, aby zaspokoić najbardziej złożony przypadek użycia, dlatego biblioteki te były skomplikowane w konfiguracji i integracji. Z biegiem czasu stawały się coraz bardziej złożone, ponieważ gromadziły cechy. Rozwój korporacyjny Java stopniowo stawał się coraz bardziej związany z łączeniem bibliotek stron trzecich, a mniej z pisaniem algorytmów.
W końcu żmudna konfiguracja i zarządzanie narzędziami dla przedsiębiorstw stało się na tyle bolesne, że pojawiły się frameworki, zwłaszcza Spring, do zarządzania. Teorię można umieścić w jednym miejscu, a narzędzie konfiguracyjne skonfiguruje elementy i połączy je ze sobą. Niestety te „ramy szkieletowe” dodały więcej abstrakcji i złożoności na całej kuli wosku.
W ciągu ostatnich kilku lat zyskało na popularności więcej lekkich bibliotek. Niemniej jednak całe pokolenie programistów Java osiągnęło pełnoletność podczas rozwoju ciężkich platform korporacyjnych. Ich wzorce do naśladowania, twórcy frameworków, napisali fabryki fabryk i moduły ładujące konfigurację proxy. Musieli konfigurować i integrować te potworności z dnia na dzień. W rezultacie kultura społeczności jako całości podążała za przykładem tych ram i miała tendencję do nadmiernego nadmiaru inżynierii.
źródło
for
pętli, gdymap
byłoby bardziej odpowiednie (i czytelne). Jest szczęśliwy środek przekazu.Wierzę, że mam odpowiedź na jedną z podniesionych przez ciebie kwestii, która nie została podniesiona przez innych.
Java optymalizuje się pod kątem wykrywania błędów programisty podczas kompilacji, nigdy nie przyjmując żadnych założeń
Ogólnie rzecz biorąc, Java zwykle wnioskuje o kodzie źródłowym tylko wtedy, gdy programista już wyraźnie wyraził swoją intencję. Kompilator Java nigdy nie przyjmuje żadnych założeń dotyczących kodu i użyje wnioskowania tylko w celu zmniejszenia zbędnego kodu.
Powodem tej filozofii jest to, że programista jest tylko człowiekiem. To, co piszemy, nie zawsze jest tym, co zamierzamy zrobić. Język Java próbuje złagodzić niektóre z tych problemów, zmuszając deweloperów do jawnego deklarowania typów. Jest to tylko sposób podwójnego sprawdzenia, czy napisany kod rzeczywiście działa zgodnie z przeznaczeniem.
Niektóre inne języki wzmacniają tę logikę jeszcze bardziej, sprawdzając warunki wstępne, warunki końcowe i niezmienniki (chociaż nie jestem pewien, czy robią to w czasie kompilacji). Są to jeszcze bardziej ekstremalne sposoby dla programisty, aby kompilator dwukrotnie sprawdził swoją pracę.
W twoim przypadku oznacza to, że aby kompilator mógł zagwarantować, że zwracasz typy, które Twoim zdaniem zwracasz, musisz przekazać te informacje kompilatorowi.
W Javie można to zrobić na dwa sposoby:
Użyj
Triplet<A, B, C>
jako typ zwracany (co naprawdę powinno byćjava.util
, a ja naprawdę nie można wyjaśnić, dlaczego tak nie jest. Zwłaszcza, że JDK8 wprowadzićFunction
,BiFunction
,Consumer
,BiConsumer
, itd ... Po prostu wydaje się, żePair
iTriplet
przynajmniej miałoby sensu. Ale błądzić )W tym celu utwórz własny typ wartości, w którym każde pole będzie odpowiednio nazwane i wpisane.
W obu przypadkach kompilator może zagwarantować, że funkcja zwróci zadeklarowane typy, a program wywołujący zda sobie sprawę z rodzaju każdego zwracanego pola i odpowiednio je wykorzysta.
Niektóre języki zapewniają statyczne sprawdzanie typu ORAZ wnioskowanie o typie w tym samym czasie, ale to pozostawia otwarte drzwi dla subtelnej klasy problemów z niedopasowaniem typów. Tam, gdzie programista chciał zwrócić wartość określonego typu, ale faktycznie zwraca inną, a kompilator STILL akceptuje kod, ponieważ zdarza się, że przez przypadek zarówno funkcja, jak i program wywołujący używają metod, które można zastosować do obu zamierzonych i rzeczywiste typy.
Rozważ coś takiego w Typescript (lub typie przepływu), w którym zamiast jawnego pisania używa się wnioskowania o typie.
Jest to oczywiście głupia trywialna sprawa, ale może być o wiele bardziej subtelna i prowadzić do problemów z debugowaniem, ponieważ kod wygląda poprawnie. Tego rodzaju problemów w Javie unika się całkowicie i na tym właśnie polega cały język.
Zauważ, że zgodnie z właściwymi zasadami obiektowymi i modelowaniem opartym na domenie, parsowanie czasu trwania w powiązanym pytaniu może również zostać zwrócone jako
java.time.Duration
obiekt, co byłoby o wiele bardziej wyraźne niż w obu powyższych przypadkach.źródło
Java i Python to dwa języki, których najczęściej używam, ale idę z innej strony. To znaczy, byłem głęboko w świecie Java, zanim zacząłem używać Pythona, więc może będę mógł pomóc. Myślę, że odpowiedź na większe pytanie „dlaczego rzeczy są tak ciężkie” sprowadza się do dwóch rzeczy:
yield
.Java „optymalizuje” oprogramowanie o wysokiej wartości, które będzie utrzymywane przez wiele lat przez duże zespoły ludzi. Mam doświadczenie w pisaniu rzeczy w Pythonie i patrzę na to rok później i jestem zaskoczony własnym kodem. W Javie mogę patrzeć na małe fragmenty kodu innej osoby i od razu wiedzieć, co ona robi. W Pythonie tak naprawdę nie można tego zrobić. Nie jest tak, że jeden jest lepszy, jak się wydaje, ale po prostu mają inne koszty.
W konkretnym przypadku, o którym wspominasz, nie ma krotek. Łatwym rozwiązaniem jest stworzenie klasy o wartościach publicznych. Kiedy Java pojawiła się po raz pierwszy, ludzie robili to dość regularnie. Pierwszym problemem jest to, że jest to ból głowy związany z konserwacją. Jeśli potrzebujesz dodać logikę lub bezpieczeństwo wątku lub chcesz użyć polimorfizmu, musisz przynajmniej dotknąć każdej klasy, która wchodzi w interakcję z tym obiektem typu „krotka”. W Pythonie istnieją rozwiązania tego typu, takie jak
__getattr__
itp., Więc nie jest to takie straszne.Istnieją jednak pewne złe nawyki (IMO). W tym przypadku, jeśli chcesz krotkę, zastanawiam się, dlaczego zrobiłbyś z niej obiekt zmienny. Potrzebujesz tylko getterów (na marginesie, nie znoszę konwencji get / set, ale tak właśnie jest.) Myślę, że naga klasa (zmienna lub nie) może być przydatna w kontekście prywatnym lub prywatnym pakietem w Javie . Oznacza to, że ograniczając odwołania do projektu do klasy, można w razie potrzeby później dokonać refaktoryzacji bez zmiany publicznego interfejsu klasy. Oto przykład, jak stworzyć prosty niezmienny obiekt:
To rodzaj wzoru, którego używam. Jeśli nie używasz IDE, powinieneś zacząć. Wygeneruje on pobierające (i ustawiające, jeśli ich potrzebujesz) dla ciebie, więc nie jest to tak bolesne.
Czułbym się niedbale, gdybym nie wskazał, że istnieje już typ, który wydaje się spełniać większość twoich potrzeb tutaj . Odkładając to na bok, stosowane przez ciebie podejście nie jest dobrze dostosowane do Javy, ponieważ dotyczy jej słabości, a nie mocnych stron. Oto proste ulepszenie:
źródło
Zanim wściekniesz się na Javę, przeczytaj moją odpowiedź w innym poście .
Jednym z twoich zarzutów jest potrzeba stworzenia klasy, aby zwrócić pewien zestaw wartości jako odpowiedź. To ważna kwestia, która, jak sądzę, pokazuje, że intuicje programistyczne są na właściwej drodze! Myślę jednak, że inne odpowiedzi nie mają znaczenia, ponieważ trzymają się prymitywnego anty-wzorca obsesji, do którego się zobowiązaliście. A Java nie ma takiej samej łatwości pracy z wieloma prymitywami, jak Python, w której możesz zwrócić wiele wartości natywnie i łatwo przypisać je do wielu zmiennych.
Ale kiedy zaczniesz myśleć o tym, co
ApproximateDuration
robi dla ciebie typ, zdajesz sobie sprawę, że nie jest on tak zawężony do „tylko pozornie niepotrzebnej klasy, aby zwrócić trzy wartości”. Pojęcie reprezentowane przez tę klasę jest w rzeczywistości jedną z podstawowych koncepcji biznesowych w domenie - trzeba być w stanie przedstawić czasy w przybliżeniu i porównać je. Musi to być częścią wszechobecnego języka rdzenia aplikacji, z dobrą obsługą obiektów i domen, aby można go było przetestować, modułowo, wielokrotnie używać i był użyteczny.Czy Twój kod, który sumuje przybliżone czasy trwania razem (lub czasy trwania z marginesem błędu, jakkolwiek to reprezentujesz) jest w pełni proceduralny, czy jest w tym jakiś przedmiot? Sugerowałbym, że dobry projekt polegający na sumowaniu przybliżonych czasów trwania narzucałby robienie tego poza dowolnym kodem, w ramach klasy, którą można przetestować. Myślę, że użycie tego rodzaju obiektu domeny będzie miało pozytywny efekt falowania w kodzie, który pomoże oderwać się od kroków proceduralnych wiersz po linii w celu wykonania pojedynczego zadania wysokiego poziomu (choć z wieloma obowiązkami) w kierunku klas z jedną odpowiedzialnością które są wolne od konfliktów różnych obaw.
Załóżmy na przykład, że dowiadujesz się więcej o tym, jaka dokładność lub skala jest faktycznie wymagana do poprawnego sumowania czasu trwania i porównania, i okazuje się, że potrzebujesz pośredniej flagi wskazującej „błąd około 32 milisekund” (blisko kwadratu pierwiastek z 1000, czyli w połowie logarytmicznie między 1 a 1000). Jeśli związałeś się z kodem, który reprezentuje to za pomocą prymitywów, musisz znaleźć każde miejsce w kodzie, w którym go masz,
is_in_seconds,is_under_1ms
i zmienić go nais_in_seconds,is_about_32_ms,is_under_1ms
. Wszystko musiałoby się zmienić w każdym miejscu! Utworzenie klasy, której obowiązkiem jest rejestrowanie marginesu błędu, aby można go było wykorzystać w innym miejscu, uwalnia konsumentów od poznania szczegółowych informacji o marginesach błędu mających znaczenie lub o tym, jak się łączą, i pozwala im tylko określić odpowiedni margines błędu, który jest istotny w tym momencie. (Oznacza to, że żaden konsumpcyjny kod, którego margines błędu jest poprawny, nie jest zmuszany do zmiany po dodaniu nowego marginesu poziomu błędu w klasie, ponieważ wszystkie stare marginesy błędu są nadal ważne).Oświadczenie końcowe
Skarga na ciężkość Javy wydaje się wtedy znikać, gdy zbliżasz się do zasad SOLID i GRASP oraz bardziej zaawansowanej inżynierii oprogramowania.
Uzupełnienie
Dodam całkowicie nieuzasadnione i niesprawiedliwe, że automatyczne właściwości C # i zdolność do przypisywania właściwości tylko do odczytu w konstruktorach pomagają jeszcze bardziej wyczyścić nieco niechlujny kod, którego będzie wymagał „sposób Java” (z wyraźnymi prywatnymi polami zaplecza i funkcjami getter / setter) :
Oto implementacja Java powyższego:
To jest naprawdę cholernie czyste. Zwróć uwagę na bardzo ważne i celowe wykorzystanie niezmienności - wydaje się to kluczowe dla tego szczególnego rodzaju klasy niosącej wartość.
Jeśli chodzi o tę kwestię, klasa ta jest również dobrym kandydatem do bycia
struct
typem wartości. Niektóre testy wykażą, czy przejście na strukturę przynosi korzyści w zakresie wydajności w czasie wykonywania (może).źródło
Zarówno Python, jak i Java są zoptymalizowane pod kątem łatwości konserwacji zgodnie z filozofią ich projektantów, ale mają bardzo różne pomysły na to, jak to osiągnąć.
Python jest językiem o wielu paradygmatach, który optymalizuje pod kątem przejrzystości i prostoty kodu (łatwy do odczytu i zapisu).
Java (tradycyjnie) jest językiem OO opartym na pojedynczym paradygmacie, który optymalizuje pod kątem jawności i spójności - nawet kosztem bardziej szczegółowego kodu.
Krotka w języku Python to struktura danych o stałej liczbie pól. Tę samą funkcjonalność można uzyskać za pomocą zwykłej klasy z wyraźnie zadeklarowanymi polami. W Pythonie naturalne jest dostarczanie krotek jako alternatywy dla klas, ponieważ pozwala to znacznie uprościć kod, szczególnie ze względu na wbudowaną obsługę składni krotek.
Ale to tak naprawdę nie pasuje do kultury Java, aby zapewnić takie skróty, ponieważ można już używać jawnie zadeklarowanych klas. Nie trzeba wprowadzać innego rodzaju struktury danych, aby zapisać niektóre wiersze kodu i uniknąć niektórych deklaracji.
Java preferuje jedną koncepcję (klasy) konsekwentnie stosowaną przy minimalnej ilości cukru syntaktycznego w specjalnych przypadkach, podczas gdy Python zapewnia wiele narzędzi i wiele cukru syntaktycznego, abyś mógł wybrać najwygodniejszy do określonego celu.
źródło
Nie szukaj praktyk; jest to zazwyczaj zły pomysł, jak powiedziano w ZŁYCH najlepszych praktykach, wzorce DOBRE? . Wiem, że nie prosisz o najlepsze praktyki, ale nadal uważam, że znajdziesz tam kilka istotnych elementów.
Poszukiwanie rozwiązania problemu jest lepsze niż praktyka, a twoim problemem nie jest krotka, aby szybko zwrócić trzy wartości w Javie:
Tutaj masz niezmienny obiekt zawierający tylko twoje dane i publiczny, więc nie ma potrzeby pobierania. Zauważ jednak, że jeśli używasz niektórych narzędzi do serializacji lub warstwy trwałości, takich jak ORM, zwykle używają one getter / setter (i mogą zaakceptować parametr, aby użyć pól zamiast getter / setter). I dlatego te praktyki są często stosowane. Więc jeśli chcesz wiedzieć o praktykach, lepiej zrozumieć, dlaczego są tutaj, aby je lepiej wykorzystać.
Wreszcie: używam programów pobierających, ponieważ używam wielu narzędzi do serializacji, ale też ich nie piszę; Używam lombok: używam skrótów dostarczonych przez moje IDE.
źródło
Ogólne informacje o idiomach Java:
Istnieje wiele powodów, dla których Java ma klasy do wszystkiego. O ile mi wiadomo, głównym powodem jest:
Java powinna być łatwa do nauczenia dla początkujących. Im bardziej wyraźne są rzeczy, tym trudniej jest przeoczyć ważne szczegóły. Zdarza się mniej magii, która byłaby trudna do zrozumienia dla początkujących.
Jeśli chodzi o twój konkretny przykład: linia argumentu dla oddzielnej klasy jest następująca: jeśli te trzy rzeczy są ze sobą wystarczająco silnie powiązane, że są zwracane jako jedna wartość, warto nazwać to „rzecz”. A wprowadzenie nazwy grupy rzeczy, które mają wspólną strukturę, oznacza zdefiniowanie klasy.
Możesz zmniejszyć płytę grzewczą za pomocą narzędzi takich jak Lombok:
źródło
Istnieje wiele rzeczy, które można powiedzieć o kulturze Java, ale myślę, że w przypadku, gdy masz teraz do czynienia, istnieje kilka istotnych aspektów:
Klasy „Struct” z polami
Jak wspomniano w innych odpowiedziach, możesz po prostu użyć klasy z polami publicznymi. Jeśli dokonasz tych ostatecznych, otrzymasz niezmienną klasę i zainicjujesz je za pomocą konstruktora:
Oczywiście oznacza to, że jesteś przywiązany do określonej klasy i wszystko, co kiedykolwiek musi wytworzyć lub skonsumować wynik analizy, musi korzystać z tej klasy. W przypadku niektórych aplikacji nie ma problemu. Dla innych może to powodować ból. Znaczna część kodu Java dotyczy definiowania umów, a to zazwyczaj prowadzi do interfejsów.
Inną pułapką jest to, że przy podejściu klasowym ujawniasz pola i wszystkie te pola muszą mieć wartości. Np. IsSeconds i millis zawsze muszą mieć jakąś wartość, nawet jeśli isLessThanOneMilli jest prawdą. Jaka powinna być interpretacja wartości pola millis, gdy jest prawdą isLessThanOneMilli?
„Struktury” jako interfejsy
Dzięki metodom statycznym dozwolonym w interfejsach tworzenie niezmiennych typów jest stosunkowo łatwe bez dużego nakładu syntaktycznego. Mogę na przykład zaimplementować strukturę wyników, o której mówisz, w następujący sposób:
Zgadzam się z tym, to wciąż bardzo dużo rzeczy do zrobienia, ale jest też kilka korzyści i myślę, że zaczną odpowiadać na niektóre z waszych głównych pytań.
O strukturze jak tego wyniku parsowania The kontrakt Twojego parsera jest bardzo jasno określone. W Pythonie jedna krotka tak naprawdę nie różni się od innej krotki. W Javie dostępne jest pisanie statyczne, więc już wykluczamy pewne klasy błędów. Na przykład, jeśli zwracasz krotkę w Pythonie i chcesz zwrócić krotkę (millis, isSeconds, isLessThanOneMilli), możesz przypadkowo zrobić:
kiedy miałeś na myśli:
Dzięki tego rodzaju interfejsowi Java nie można kompilować:
w ogóle. Musisz zrobić:
Jest to ogólna zaleta języków statycznych.
To podejście zaczyna także dawać Ci możliwość ograniczenia, jakie wartości możesz uzyskać. Na przykład, wywołując getMillis (), możesz sprawdzić, czy isLessThanOneMilli () jest prawdą, a jeśli tak, wyrzuć IllegalStateException (na przykład), ponieważ w tym przypadku nie ma znaczącej wartości millis.
Utrudnianie robienia złych rzeczy
W powyższym przykładzie interfejsu nadal występuje problem, że można przypadkowo zamienić argumenty isSeconds i isLessThanOneMilli, ponieważ mają one ten sam typ.
W praktyce naprawdę warto skorzystać z TimeUnit i czasu trwania, aby uzyskać wynik taki jak:
To będzie o wiele więcej kodu, ale musisz go napisać tylko raz, i (zakładając, że właściwie udokumentowałeś rzeczy), ludzie, którzy ostatecznie używają twojego kodu, nie muszą zgadywać, co oznacza wynik, i nie mogę przypadkowo zrobić czegoś takiego, jak
result[0]
mają na myśliresult[1]
. Nadal możesz tworzyć instancje dość zwięźle, a uzyskiwanie z nich danych nie jest wcale takie trudne:Zauważ, że możesz zrobić coś takiego również z podejściem klasowym. Po prostu określ konstruktory dla różnych przypadków. Nadal masz problem z tym, co zainicjować inne pola i nie możesz uniemożliwić dostępu do nich.
Inna odpowiedź wspomniała, że Java typu korporacyjnego oznacza, że przez większość czasu tworzysz inne biblioteki, które już istnieją, lub piszesz biblioteki dla innych osób. Twój publiczny interfejs API nie powinien wymagać dużo czasu od zapoznania się z dokumentacją, aby odszyfrować typy wyników, jeśli można tego uniknąć.
Te struktury piszesz tylko raz, ale tworzysz je wiele razy, więc nadal chcesz tego zwięzłego tworzenia (które otrzymujesz). Wpisywanie statyczne zapewnia, że dane, które z nich pobierasz, są zgodne z oczekiwaniami.
Teraz jednak powiedziano, że wciąż istnieją miejsca, w których proste krotki lub listy mogą mieć sens. Zwracanie tablicy czegoś może być mniejsze, a jeśli tak jest (a narzut jest znaczny, co można określić za pomocą profilowania), użycie wewnętrznej tablicy prostych wartości może mieć sens. Twój publiczny interfejs API powinien prawdopodobnie mieć jasno zdefiniowane typy.
źródło
originalTimeUnit=DurationTimeUnit.UNDER1MS
wywołujący próbował odczytać wartość milisekund.Problem polega na tym, że porównujesz jabłka do pomarańczy . Zapytałeś, jak symulować zwracanie więcej niż pojedynczej wartości, podając szybki i brudny przykład python z krotką bez typu i faktycznie otrzymałeś odpowiedź praktycznie jednej linii .
Przyjęta odpowiedź stanowi prawidłowe rozwiązanie biznesowe. Bez szybkiego tymczasowego obejścia, które musiałbyś wyrzucić i poprawnie zaimplementować za pierwszym razem, musisz zrobić cokolwiek praktycznego z zwróconą wartością, ale klasa POJO, która jest kompatybilna z dużym zestawem bibliotek, w tym trwałością, serializacją / deserializacją, oprzyrządowanie i wszystko możliwe.
To też nie jest długo. Jedyne, co musisz napisać, to definicje pól. Setery, gettery, hashCode i równe mogą być generowane. Tak więc twoje rzeczywiste pytanie powinno brzmieć: dlaczego pobierające i ustawiające nie są generowane automatycznie, ale jest to kwestia składniowa (powiedzmy, że kwestia cukru syntaktycznego), a nie kwestia kulturowa.
I wreszcie zastanawiasz się nad przyspieszeniem czegoś, co wcale nie jest ważne. Czas spędzony na pisaniu klas DTO jest nieznaczny w porównaniu do czasu poświęconego na utrzymanie i debugowanie systemu. Dlatego nikt nie optymalizuje pod kątem mniejszej szczegółowości.
źródło
Są trzy różne czynniki, które przyczyniają się do tego, co obserwujesz.
Krotki kontra nazwane pola
Być może najbardziej trywialny - w innych językach używałeś krotki. Debata, czy krotki są dobrym pomysłem, nie jest tak naprawdę istotna - ale w Javie użyłeś cięższej struktury, więc jest to nieco niesprawiedliwe porównanie: mogłeś użyć tablicy obiektów i rzutowania typu.
Składnia języka
Czy łatwiej byłoby zadeklarować klasę? Nie mówię o upublicznianiu pól ani korzystaniu z mapy, ale coś w rodzaju klas przypadków Scali, które zapewniają wszystkie zalety opisanej konfiguracji, ale są bardziej zwięzłe:
Możemy to mieć - ale wiąże się to z pewnym kosztem: składnia staje się bardziej skomplikowana. Oczywiście może być warto w niektórych przypadkach, a nawet w większości przypadków, a nawet w większości przypadków na następne 5 lat - ale należy to ocenić. Nawiasem mówiąc, jest to jedna z fajnych rzeczy w językach, które możesz modyfikować (np. Lisp) - i zwróć uwagę, jak to staje się możliwe dzięki prostocie składni. Nawet jeśli tak naprawdę nie modyfikujesz języka, prosta składnia umożliwia użycie bardziej zaawansowanych narzędzi; na przykład wiele razy brakuje mi niektórych opcji refaktoryzacji dostępnych dla Javy, ale nie dla Scali.
Filozofia językowa
Ale najważniejsze jest to, że język powinien umożliwiać określony sposób myślenia. Czasami może to być uciążliwe (często chciałem mieć wsparcie dla pewnej funkcji), ale usuwanie funkcji jest równie ważne, jak ich posiadanie. Czy możesz wesprzeć wszystko? Jasne, ale równie dobrze możesz napisać kompilator, który kompiluje każdy język. Innymi słowy, nie będziesz mieć języka - będziesz miał superset języków, a każdy projekt będzie zasadniczo przyjmował podzbiór.
Oczywiście można napisać kod niezgodny z filozofią języka i, jak zauważyłeś, wyniki są często brzydkie. Posiadanie klasy z zaledwie kilkoma polami w Javie jest podobne do używania var w Scali, degeneracji predykatów prologu do funkcji, wykonywania niebezpiecznych operacji w haskell itp. Klasy Java nie są przeznaczone do lekkości - nie służą do przesyłania danych . Kiedy coś wydaje się trudne, często owocne jest odsunięcie się i sprawdzenie, czy istnieje inny sposób. W twoim przykładzie:
Dlaczego czas trwania różni się od jednostek? Istnieje wiele bibliotek czasowych, które pozwalają zadeklarować czas trwania - coś takiego jak Czas trwania (5 sekund) (składnia będzie się zmieniać), co pozwoli ci robić, co chcesz, w znacznie bardziej solidny sposób. Może chcesz go przekonwertować - po co sprawdzać, czy wynik [1] (lub [2]?) To „godzina” i pomnożenie przez 3600? A jeśli chodzi o trzeci argument - jaki jest jego cel? Sądzę, że w pewnym momencie będziesz musiał wydrukować „mniej niż 1ms” lub rzeczywisty czas - to logika, która naturalnie należy do danych czasowych. Tj. Powinieneś mieć taką klasę:
}
lub cokolwiek, co naprawdę chcesz zrobić z danymi, stąd enkapsulacja logiki.
Oczywiście może się zdarzyć, że ten sposób nie zadziała - nie mówię, że jest to magiczny algorytm do konwersji wyników krotek na idiomatyczny kod Java! I mogą zdarzyć się przypadki, gdy jest to bardzo brzydkie i złe i być może powinieneś był użyć innego języka - dlatego w końcu jest ich tak wiele!
Ale mój pogląd na to, dlaczego klasy są „ciężkimi strukturami” w Javie, jest taki, że nie należy ich używać jako kontenerów danych, ale jako samodzielne komórki logiki.
źródło
O ile mi wiadomo, najważniejsze są następujące powody
To daje ci „Musisz napisać klasę, aby zwrócić za nietrywialny wynik”, co z kolei jest dość ciężkie. Jeśli użyłeś klasy zamiast interfejsu, możesz po prostu mieć pola i używać ich bezpośrednio, ale to wiąże cię z konkretną implementacją.
źródło
def f(...): (Int, Int)
Jest funkcją,f
która zwraca wartość, która przypadkowo jest krotką liczb całkowitych). Nie jestem pewien, czy problemami są generycy w Javie; zwróć uwagę, że Haskell również usuwa na przykład skasowanie tekstu. Nie sądzę, że istnieje techniczny powód, dla którego Java nie ma krotek.Zgadzam się z odpowiedzią JacquesB na to
Ale jawność i konsekwencja nie są końcowymi celami, które należy zoptymalizować. Kiedy mówisz, że „Python jest zoptymalizowany pod kątem czytelności”, natychmiast wspominasz, że celem końcowym jest „łatwość konserwacji” i „szybkość rozwoju”.
Co osiągniesz, gdy będziesz miał jawność i spójność, zrobione w sposób Java? Uważam, że ewoluował jako język, który twierdzi, że zapewnia przewidywalny, spójny, jednolity sposób rozwiązania każdego problemu z oprogramowaniem.
Innymi słowy, kultura Java jest zoptymalizowana pod kątem przekonania menedżerów, że rozumieją tworzenie oprogramowania.
Lub, jak ujął to pewien mądry facet bardzo dawno temu ,
źródło
(Ta odpowiedź nie jest wyjaśnieniem w szczególności dla Javy, ale odnosi się do ogólnego pytania „Co mogą zoptymalizować [ciężkie] praktyki?”)
Rozważ te dwie zasady:
Próba zoptymalizowania jednego z tych celów może czasami przeszkadzać drugiemu (tj . Utrudnienie zrobienia niewłaściwej rzeczy może również utrudnić wykonanie właściwej rzeczy lub odwrotnie).
Wybór kompromisów w konkretnym przypadku zależy od aplikacji, decyzji programistów lub zespołu oraz kultury (organizacji lub społeczności językowej).
Na przykład, jeśli błąd lub kilka godzin przerwy w programie może spowodować utratę życia (systemy medyczne, aeronautyka), a nawet zwykłe pieniądze (jak miliony dolarów, powiedzmy, systemy reklamowe Google), zrobiłbyś różne kompromisy (nie tylko w twoim języku, ale także w innych aspektach kultury inżynieryjnej) niż w przypadku jednorazowego skryptu: prawdopodobnie skłania się ku „ciężkiej” stronie.
Inne przykłady, które sprawiają, że twój system jest bardziej „ciężki”:
To tylko niektóre przykłady, aby dać ci wyobrażenie o przypadkach, w których uczynienie rzeczy „ciężkimi” (i utrudniającymi szybkie napisanie kodu) mogą być rzeczywiście zamierzone. (Ktoś mógłby nawet argumentować, że jeśli pisanie kodu wymaga dużego wysiłku, może to prowadzić do bardziej ostrożnego myślenia przed napisaniem kodu! Oczywiście ta linia argumentów szybko staje się śmieszna.)
Przykład: wewnętrzny system Python firmy Google sprawia, że rzeczy stają się „ciężkie”, tak że nie można po prostu zaimportować kodu innej osoby, należy zadeklarować zależność w pliku BUILD , zespół, którego kod chcesz zaimportować, musi zadeklarować swoją bibliotekę jako widoczne dla twojego kodu itp.
Uwaga : wszystkie powyższe dotyczą tylko sytuacji, gdy rzeczy stają się „ciężkie”. Absolutnie nie twierdzę, że Java lub Python (same języki lub ich kultury) powodują optymalne kompromisy w konkretnym przypadku; o tym powinieneś pomyśleć. Dwa powiązane linki dotyczące takich kompromisów:
źródło
Kultura Java ewoluowała z czasem pod dużym wpływem zarówno środowisk open source, jak i środowisk korporacyjnych - co jest dziwnym połączeniem, jeśli naprawdę się nad tym zastanowisz. Rozwiązania dla przedsiębiorstw wymagają ciężkich narzędzi, a open source wymaga prostoty. W rezultacie Java jest gdzieś pośrodku.
Częścią tego, co wpływa na zalecenie, jest to, co w Pythonie jest uważane za czytelne i możliwe do utrzymania, a Java jest zupełnie inna.
Wspominam tylko o C #, ponieważ standardowa biblioteka ma zestaw klas Tuple <A, B, C, .. n> i jest to doskonały przykład tego, jak nieporęczne są krotki, jeśli język nie obsługuje ich bezpośrednio. W prawie każdym przypadku kod staje się bardziej czytelny i łatwy w utrzymaniu, jeśli masz dobrze wybrane klasy do obsługi problemu. W konkretnym przykładzie w połączonym pytaniu o przepełnienie stosu pozostałe wartości można łatwo wyrazić jako obiekty pobierające na obiekcie zwracanym.
Ciekawym rozwiązaniem, które zrobiła platforma C #, która zapewnia szczęśliwy środek, jest pomysł anonimowych obiektów (wydanych w C # 3.0 ), które dość dobrze zarysowują swędzenie. Niestety Java nie ma jeszcze odpowiednika.
Dopóki funkcje języka Java nie zostaną zmienione, najbardziej czytelnym i łatwym do utrzymania rozwiązaniem jest posiadanie dedykowanego obiektu. Wynika to z ograniczeń w języku, które sięgają początków w 1995 roku. Oryginalni autorzy zaplanowali wiele innych funkcji językowych, które nigdy tego nie zrobiły, a kompatybilność wsteczna jest jednym z głównych ograniczeń ewolucji Javy w czasie.
źródło
Myślę, że jedną z kluczowych rzeczy związanych z użyciem klasy w tym przypadku jest to, że to, co idzie razem, powinno pozostać razem.
Miałem tę dyskusję na odwrót, dotyczącą argumentów metod: Rozważ prostą metodę, która oblicza BMI:
W tym przypadku sprzeciwiłbym się temu stylowi, ponieważ waga i wzrost są powiązane. Metoda „komunikuje się”, gdy nie są to dwie osobne wartości. Kiedy obliczyłbyś BMI z wagą jednej osoby i wzrostem innej osoby? To nie miałoby sensu.
Ma o wiele większy sens, ponieważ teraz wyraźnie komunikujesz, że wzrost i waga pochodzą z tego samego źródła.
To samo dotyczy zwracania wielu wartości. Jeśli są wyraźnie połączone, zwróć schludny mały pakiet i użyj obiektu, jeśli nie zwracają wielu wartości.
źródło
Szczerze mówiąc, kultura polega na tym, że programiści Java zwykle wychodzili z uniwersytetów, w których nauczano zasad obiektowych i zasad zrównoważonego projektowania oprogramowania.
Jak ErikE mówi w swoich słowach w większej odpowiedzi, wydaje się, że nie piszesz zrównoważonego kodu. To, co widzę w twoim przykładzie, jest bardzo niezręczne.
W kulturze Java będziesz zdawał sobie sprawę z tego, jakie biblioteki są dostępne, a to pozwoli ci osiągnąć o wiele więcej niż nieszablonowe programowanie. Wymieniłeś więc swoje osobliwości na wzorce projektowe i style, które zostały wypróbowane i przetestowane w trudnych warunkach przemysłowych.
Ale, jak mówisz, nie jest to pozbawione wad: dzisiaj, po korzystaniu z Javy od ponad 10 lat, zwykle używam Node / JavaScript lub Go do nowych projektów, ponieważ oba umożliwiają szybszy rozwój, a przy architekturze w stylu mikrousług są to często wystarczy. Sądząc po tym, że Google najpierw intensywnie korzystał z Javy, ale był pomysłodawcą Go, myślę, że mogą robić to samo. Ale mimo że teraz używam Go i JavaScript, wciąż korzystam z wielu umiejętności projektowania, które zdobyłem od lat używania i rozumienia Javy.
źródło