Grokkingowa kultura Java - dlaczego rzeczy są tak ciężkie? Do czego to optymalizuje? [Zamknięte]

288

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.

Michaił Ramendik
źródło
1
nie odpowiedzieć na techniczne aspekty pytania, ale nadal w kontekście: softwareengineering.stackexchange.com/a/63145/13899
Newtopian
74
Możesz przeczytać esej Steve'a Yeggea Egzekucja w Królestwie Rzeczowników
pułkownik Panic
3
Oto, co uważam za właściwą odpowiedź. Nie żartujesz z Javy, ponieważ myślisz o programowaniu jak sysadmin, a nie programista. Dla ciebie oprogramowanie jest czymś, czego używasz do osiągnięcia określonego zadania w jak najbardziej odpowiedni sposób. Piszę kod Java od 20 lat, a niektóre projekty, nad którymi pracowałem, zajęły zespołowi 20, 2 lata. Java nie zastępuje Pythona i odwrotnie. Oboje wykonują swoją pracę, ale ich praca jest zupełnie inna.
Richard
1
Powodem, dla którego Java jest de facto standardem jest to, że jest po prostu właściwa. Po raz pierwszy stworzyli go poważni ludzie, którzy mieli brody, zanim brody były modne. Jeśli jesteś przyzwyczajony do podskakiwania skryptów powłoki w celu manipulowania plikami, Java wydaje się nie do przyjęcia. Ale jeśli chcesz utworzyć podwójny nadmiarowy klaster serwerów, który może obsługiwać 50 milionów osób dziennie, wspierany przez klastrowane buforowanie redis, 3 systemy rozliczeniowe i klaster bazy danych o wartości 20 milionów funtów wyroczni. Nie będziesz używać Pythona, nawet jeśli uzyskasz dostęp baza danych w Pythonie zawiera 25 linii mniej kodu.
Richard

Odpowiedzi:

237

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.

Sekret Solomonoffa
źródło
15
Zabawne jest to, że inne języki wydają się zbierać pewne „java-isms”. Funkcje PHP OO są mocno zainspirowane Javą, a obecnie JavaScript jest często krytykowany za ogromną ilość frameworków i jak ciężko jest zebrać wszystkie zależności i uruchomić nową aplikację.
marcu
14
@marcus może dlatego, że niektórzy ludzie w końcu nauczyli się rzeczy „nie wymyślaj koła ponownie”? Zależności są przecież kosztem nie wynalezienia koła
Walfrat 28.04.17
9
„Terse” często przekłada się na tajemne, „sprytne” , jedno-liniowe kody do golfa.
Tulains Córdova
7
Przemyślana, dobrze napisana odpowiedź. Kontekst historyczny. Względnie niepopionowane. Ładnie wykonane.
Cheezmeister
10
@ TulainsCórdova Zgadzam się, że powinniśmy „unikać… sprytnych sztuczek, takich jak plaga” (Donald Knuth), ale to nie oznacza pisania forpętli, gdy mapbyłoby bardziej odpowiednie (i czytelne). Jest szczęśliwy środek przekazu.
Brian McCutchon
73

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:

  1. 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ę, że Pairi Tripletprzynajmniej miałoby sensu. Ale błądzić )

  2. 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.

function parseDurationInMillis(csvLine){
    // here the developer intends to return a Number, 
    // but actually it's a String
    return csv.firstField();
}

// Compiler infers that parseDurationInMillis is String, so it does
// string concatenation and infers that plusTwoSeconds is String
// Developer actually intended Number
var plusTwoSeconds = 2000 + parseDurationInMillis(csvLine);

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.Durationobiekt, co byłoby o wiele bardziej wyraźne niż w obu powyższych przypadkach.

LordOfThePigs
źródło
38
Stwierdzenie, że Java optymalizuje poprawność kodu może być słusznym punktem, ale wydaje mi się (programowanie wielu języków, ostatnio trochę java), że język albo zawodzi, albo jest bardzo nieefektywny. To prawda, że ​​Java jest stara i wtedy wiele rzeczy nie istniało, ale istnieją nowoczesne alternatywy, które zapewniają znacznie lepszą kontrolę poprawności, takie jak Haskell (i śmiem twierdzić, że? Rust lub Go). Cała nieporadność Javy jest niepotrzebna do tego celu. - A poza tym, to nie jest w ogóle wyjaśnić kulturę, ale Solomonoff ujawnił, że kultura jest BS tak.
tomsmeding
8
Ta próbka nie pokazuje, że wnioskowanie o typie stanowi problem, ale decyzja JavaScript, aby zezwolić na niejawne konwersje w dynamicznym języku jest głupia. Jakikolwiek rozsądny język dynamiczny lub statycznie reklamowany język z wnioskiem o typie nie pozwala na to. Jako przykład dla obu rodzajów: python wyrzuciłby środowisko wykonawcze, z wyjątkiem tego, że Haskell nie skompilowałby się.
Voo,
39
@tomsmeding Warto zauważyć, że ten argument jest szczególnie głupi, ponieważ Haskell istnieje około 5 lat dłużej niż Java. Java może mieć system typów, ale nie miała nawet najnowszej weryfikacji poprawności, kiedy została wydana.
Alexis King
21
„Java optymalizuje się pod kątem wykrywania błędów w czasie kompilacji” - Nie. Zapewnia to, ale, jak zauważyli inni, z pewnością nie optymalizuje jej pod żadnym względem. Co więcej, ten argument jest całkowicie niezwiązany z pytaniem OP, ponieważ inne języki, które faktycznie optymalizują się pod kątem wykrywania błędów podczas kompilacji, mają znacznie mniej lekką składnię dla podobnych scenariuszy. Zbyt duża szczegółowość Java nie ma nic wspólnego z weryfikacją czasu kompilacji.
Konrad Rudolph
19
@KonradRudolph Tak, ta odpowiedź jest całkowicie bezsensowna, choć przypuszczam, że jest emocjonalnie pociągająca. Nie jestem pewien, dlaczego ma tyle głosów. Haskell (a może nawet bardziej znacząco Idris) optymalizuje znacznie więcej pod kątem poprawności niż Java i ma krotki z lekką składnią. Co więcej, zdefiniowanie typu danych jest jednowierszowe, a otrzymasz wszystko, co otrzymałaby wersja Java. Ta odpowiedź jest usprawiedliwieniem złego projektowania języka i niepotrzebnej gadatliwości, ale brzmi dobrze, jeśli lubisz Javę i nie znasz innych języków.
Alexis King
50

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:

  • Koszty związane z rozwojem między nimi są jak powietrze w długim balonie ze zwierzęcym balonem. Możesz wycisnąć jedną część balonu, a druga puchnie. Python ma tendencję do ściskania wczesnej części. Java ściska późniejszą część.
  • Java wciąż nie ma niektórych funkcji, które mogłyby usunąć część tego ciężaru. Java 8 zrobiła ogromne wrażenie, ale kultura nie do końca przyswoiła sobie zmiany. Java mogłaby użyć jeszcze kilku rzeczy, takich jak 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:

public class Blah 
{
  public static Blah blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    return new Blah(number, isInSeconds, lessThanOneMillis);
  }

  private final long number;
  private final boolean isInSeconds;
  private final boolean lessThanOneMillis;

  public Blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    this.number = number;
    this.isInSeconds = isInSeconds;
    this.lessThanOneMillis = lessThanOneMillis;
  }

  public long getNumber()
  {
    return number;
  }

  public boolean isInSeconds()
  {
    return isInSeconds;
  }

  public boolean isLessThanOneMillis()
  {
    return lessThanOneMillis;
  }
}

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:

public class Blah 
{
  public static Blah fromSeconds(long number)
  {
    return new Blah(number * 1000_000);
  }

  public static Blah fromMills(long number)
  {
    return new Blah(number * 1000);
  }

  public static Blah fromNanos(long number)
  {
    return new Blah(number);
  }

  private final long nanos;

  private Blah(long nanos)
  {
    this.nanos = nanos;
  }

  public long getNanos()
  {
    return nanos;
  }

  public long getMillis()
  {
    return getNanos() / 1000; // or round, whatever your logic is
  }

  public long getSeconds()
  {
    return getMillis() / 1000; // or round, whatever your logic is
  }

  /* I don't really know what this is about but I hope you get the idea */
  public boolean isLessThanOneMillis()
  {
    return getMillis() < 1;
  }
}
JimmyJames
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
wałek klonowy
2
Czy mogę zasugerować, aby spojrzeć także na Scalę w odniesieniu do „bardziej nowoczesnych” funkcji Java ...
MikeW
1
@MikeW Właściwie zacząłem od Scali już we wczesnych wydaniach i przez jakiś czas byłem aktywny na forach Scala. Myślę, że to wielkie osiągnięcie w projektowaniu języka, ale doszedłem do wniosku, że tak naprawdę nie tego szukałem i że byłem kwadratowym pegiem w tej społeczności. Powinienem na to jeszcze raz spojrzeć, ponieważ od tego czasu prawdopodobnie zmienił się znacznie.
JimmyJames
Ta odpowiedź nie dotyczy przybliżonego aspektu wartości podniesionego przez PO.
ErikE
@ErikE Słowa „przybliżona wartość” nie pojawiają się w tekście pytania. Jeśli możesz wskazać mi konkretną część pytania, na które nie odpowiedziałem, mogę spróbować.
JimmyJames
41

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 ApproximateDurationrobi 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_1msi 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) :

// Warning: C# code!
public sealed class ApproximateDuration {
   public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
      LowMilliseconds = lowMilliseconds;
      HighMilliseconds = highMilliseconds;
   }
   public int LowMilliseconds { get; }
   public int HighMilliseconds { get; }
}

Oto implementacja Java powyższego:

public final class ApproximateDuration {
  private final int lowMilliseconds;
  private final int highMilliseconds;

  public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
    this.lowMilliseconds = lowMilliseconds;
    this.highMilliseconds = highMilliseconds;
  }

  public int getLowMilliseconds() {
    return lowMilliseconds;
  }

  public int getHighMilliseconds() {
    return highMilliseconds;
  }
}

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 structtypem wartości. Niektóre testy wykażą, czy przejście na strukturę przynosi korzyści w zakresie wydajności w czasie wykonywania (może).

ErikE
źródło
9
Myślę, że to moja ulubiona odpowiedź. Przekonałem się, że kiedy promuję taką gównianą klasę wychowanków takich jak ta, w coś dorosłego, staje się domem dla wszystkich związanych z tym obowiązków i zing! wszystko robi się czystsze! I zwykle uczę się jednocześnie czegoś interesującego o przestrzeni problemowej. Oczywiście jest umiejętność unikania nadmiernej inżynierii ... ale Gosling nie był głupi, kiedy pominął składnię krotek, podobnie jak w przypadku GOTO. Twoje oświadczenie końcowe jest doskonałe. Dzięki!
SusanW,
2
Jeśli podoba Ci się ta odpowiedź, przeczytaj o projektach opartych na domenie. Ma wiele do powiedzenia na temat reprezentowania pojęć domenowych w kodzie.
neontapir
@neontapir Tak, dziękuję! Wiedziałem, że jest na to nazwa! :-) Chociaż muszę powiedzieć, że wolę, gdy koncepcja domeny pojawia się organicznie z inspirowanego fragmentu refaktoryzacji (takiego jak ten!) ... to trochę tak, jak można naprawić swój XIX-wieczny problem grawitacji poprzez odkrycie Neptuna .. .
SusanW
@ SusanW Zgadzam się. Refaktoryzacja w celu usunięcia prymitywnej obsesji może być doskonałym sposobem na odkrycie koncepcji domeny!
neontapir
Dodałem wersję Java przykładu jak omówiono. Nie interesuje mnie cały magiczny cukier syntaktyczny w C #, więc jeśli czegoś brakuje, daj mi znać.
JimmyJames
24

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.

JacquesB
źródło
+1, ale w jaki sposób łączy się to ze statycznie wpisanymi językami z klasami, takimi jak Scala, które mimo to uznały potrzebę wprowadzenia krotek? Myślę, że krotki są po prostu lepszym rozwiązaniem w niektórych przypadkach, nawet dla języków z klasami.
Andres F.
@AndresF .: Co rozumiesz przez siatkę? Scala jest językiem innym niż Java i ma inne zasady projektowania i idiomy.
JacquesB
Miałem to na myśli w odpowiedzi na twój piąty akapit, który zaczyna się od „ale to tak naprawdę nie pasuje do kultury Java [...]”. Rzeczywiście, zgadzam się, że gadatliwość jest częścią kultury Javy, ale brak krotek nie może być taki, że „już używają klas jawnie zadeklarowanych” - w końcu Scala ma również klasy jawne (i wprowadza bardziej zwięzłe klasy spraw ), ale to umożliwia także krotki! W końcu myślę, że prawdopodobnie Java nie wprowadziła krotek nie tylko dlatego, że klasy mogą osiągnąć to samo (z większym bałaganem), ale także dlatego, że jego składnia musi być znana programistom C, a C nie miał krotek.
Andres F.
@AndresF .: Scala nie ma niechęci do wielu sposobów robienia rzeczy, łączy w sobie cechy zarówno funkcjonalnego paradygmatu, jak i klasycznego OO. W ten sposób jest bliżej filozofii Python.
JacquesB
@AndresF .: Tak Java zaczęła od składni zbliżonej do C / C ++, ale bardzo uprościła. C # początkowo był prawie dokładną kopią Javy, ale przez lata dodawał wiele funkcji, w tym krotki. To naprawdę różnica w filozofii projektowania języka. (Późniejsze wersje Javy wydają się jednak mniej dogmatyczne. Funkcje wyższego rzędu zostały początkowo odrzucone, ponieważ można było zrobić to samo, które klasy, ale teraz zostały wprowadzone. Więc może widzimy zmianę w filozofii.)
JacquesB
16

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:

  • Istnieją tablice
  • Możesz zwrócić tablicę jako listę w jednej linijce: Arrays.asList (...)
  • Jeśli chcesz zachować obiekt z mniejszą możliwą płytą grzewczą (i bez lomboka):

class MyTuple {
    public final long value_in_milliseconds;
    public final boolean is_in_seconds;
    public final boolean is_under_1ms;
    public MyTuple(long value_in_milliseconds,....){
        ...
    }
 }

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.

Walfrat
źródło
9
Nadal warto rozumieć popularne idiomy, które można spotkać w różnych językach, ponieważ stają się one bardziej dostępnym de facto standardem. Te idiomy zwykle należą do „najlepszych praktyk” z jakiegokolwiek powodu.
Berin Loritsch
3
Hej, rozsądne rozwiązanie problemu. Wydaje się całkiem rozsądne, aby napisać klasę (/ struct) z kilkoma członkami publicznymi zamiast w pełni rozwiniętego, przebudowanego rozwiązania OOP z [gs] etters.
tomsmeding
3
Prowadzi to do wzdęć, ponieważ w przeciwieństwie do Pythona wyszukiwanie lub krótsze i bardziej eleganckie rozwiązanie nie jest zalecane. Ale zaletą jest to, że wzdęcie będzie mniej więcej tego samego rodzaju dla każdego programisty Java. Dlatego istnieje wyższy stopień konserwacji, ponieważ programiści są bardziej wymienni, a jeśli dołączy się dwa projekty lub przypadkowo rozejdą się dwa zespoły, walka będzie mniejsza.
Michaił Ramendik
2
@MikhailRamendik Jest to kompromis między YAGNI i OOP. Ortodoksyjny OOP mówi, że każdy obiekt powinien być wymienny; liczy się tylko publiczny interfejs obiektu. Umożliwia to pisanie kodu, który mniej koncentruje się na konkretnych obiektach, a bardziej na interfejsach; a ponieważ pola nie mogą stanowić części interfejsu, nigdy ich nie ujawnisz. Może to znacznie ułatwić utrzymanie kodu na dłuższą metę. Dodatkowo pozwala zapobiegać nieprawidłowym stanom. W oryginalnej próbce możliwe jest posiadanie obiektu, który zarówno „<ms”, jak i „s” wykluczają się wzajemnie. To źle.
Luaan
2
@PeterMortensen To biblioteka Java, która robi wiele dość niezwiązanych ze sobą rzeczy. Ale to bardzo dobrze. Oto funkcje
Michael
11

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:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}
marstato
źródło
5
Jest to również projekt AutoValue firmy Google: github.com/google/auto/blob/master/value/userguide/index.md
Ivan
Dlatego też programy Java mogą być stosunkowo łatwe w utrzymaniu!
Thorbjørn Ravn Andersen
7

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:

  1. Kod biblioteki jest zapisywany raz, ale jest używany znacznie częściej. Chociaż miło jest zminimalizować narzut związany z pisaniem biblioteki, prawdopodobnie na dłuższą metę bardziej opłacalne jest pisanie w sposób, który minimalizuje narzut związany z korzystaniem z biblioteki.
  2. Oznacza to, że typy samodokumentujące są świetne: nazwy metod pomagają wyjaśnić, co się dzieje i co wydostajesz się z obiektu.
  3. Wpisywanie statyczne jest bardzo przydatnym narzędziem do eliminowania niektórych klas błędów. Z pewnością nie naprawia wszystkiego (ludzie lubią żartować z Haskella, że ​​kiedy system typów zaakceptuje Twój kod, prawdopodobnie jest poprawny), ale bardzo łatwo uniemożliwia pewne niewłaściwe rzeczy.
  4. Pisanie kodu bibliotecznego polega na określaniu umów. Definiowanie interfejsów dla argumentów i typów wyników sprawia, że ​​granice twoich umów są jaśniej zdefiniowane. Jeśli coś akceptuje lub produkuje krotkę, nie wiadomo, czy jest to rodzaj krotki, którą powinieneś otrzymać lub wyprodukować, i jest bardzo mało przeszkód dla takiego rodzaju ogólnego (czy ma nawet odpowiednią liczbę elementów? są tego typu, którego się spodziewałeś?).

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:

   class ParseResult0 {
      public final long millis;
      public final boolean isSeconds;
      public final boolean isLessThanOneMilli;

      public ParseResult0(long millis, boolean isSeconds, boolean isLessThanOneMilli) {
         this.millis = millis;
         this.isSeconds = isSeconds;
         this.isLessThanOneMilli = isLessThanOneMilli;
      }
   }

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:

   interface ParseResult {
      long getMillis();

      boolean isSeconds();

      boolean isLessThanOneMilli();

      static ParseResult from(long millis, boolean isSeconds, boolean isLessThanOneMill) {
         return new ParseResult() {
            @Override
            public boolean isSeconds() {
               return isSeconds;
            }

            @Override
            public boolean isLessThanOneMilli() {
               return isLessThanOneMill;
            }

            @Override
            public long getMillis() {
               return millis;
            }
         };
      }
   }

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ć:

return (true, 500, false)

kiedy miałeś na myśli:

return (500, true, false)

Dzięki tego rodzaju interfejsowi Java nie można kompilować:

return ParseResult.from(true, 500, false);

w ogóle. Musisz zrobić:

return ParseResult.from(500, true, false);

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:

   interface Duration {
      TimeUnit getTimeUnit();

      long getDuration();

      static Duration from(TimeUnit unit, long duration) {
         return new Duration() {
            @Override
            public TimeUnit getTimeUnit() {
               return unit;
            }

            @Override
            public long getDuration() {
               return duration;
            }
         };
      }
   }

   interface ParseResult2 {

      boolean isLessThanOneMilli();

      Duration getDuration();

      static ParseResult2 from(TimeUnit unit, long duration) {
         Duration d = Duration.from(unit, duration);
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return false;
            }

            @Override
            public Duration getDuration() {
               return d;
            }
         };
      }

      static ParseResult2 lessThanOneMilli() {
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return true;
            }

            @Override
            public Duration getDuration() {
               throw new IllegalStateException();
            }
         };
      }
   }

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śli result[1]. Nadal możesz tworzyć instancje dość zwięźle, a uzyskiwanie z nich danych nie jest wcale takie trudne:

  ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
  ParseResult2 y = ParseResult2.lessThanOneMilli();

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.

Joshua Taylor
źródło
Na końcu użyłem własnego wyliczenia zamiast TimeUnit, ponieważ właśnie dodałem UNDER1MS wraz z rzeczywistymi jednostkami czasu. Więc teraz mam tylko dwa pola, a nie trzy. (Teoretycznie mogłem niewłaściwie wykorzystać TimeUnit.NANOSECONDS, ale byłoby to bardzo mylące.)
Michaił Ramendik
Nie utworzyłem gettera, ale teraz widzę, w jaki sposób getter pozwoliłby mi zgłosić wyjątek, gdyby originalTimeUnit=DurationTimeUnit.UNDER1MSwywołujący próbował odczytać wartość milisekund.
Michaił Ramendik
Wiem, że o tym wspomniałeś, ale pamiętaj, że twój przykład z krotkami python dotyczy naprawdę krotek dynamicznych vs. tak naprawdę niewiele mówi o samych krotkach. Państwo może być statycznie wpisane krotki których nie uda się skompilować, a jestem pewien, że wiesz :)
Andres F.
1
@ Veedrac Nie jestem pewien, co masz na myśli. Chodzi mi o to, że można pisać bardziej dokładnie w ten sposób kod biblioteki (nawet jeśli zajmuje to więcej czasu), ponieważ użycie kodu zajmie więcej czasu niż jego napisanie .
Joshua Taylor
1
@ Veedrac Tak, to prawda. Ale ja twierdzą, że nie ma rozróżnienia między kodem, który będzie sensownie użyte ponownie, i kod, który nie będzie. Na przykład w pakiecie Java niektóre klasy są publiczne i przyzwyczajają się z innych lokalizacji. Te interfejsy API powinny być dobrze przemyślane. Wewnętrznie istnieją klasy, które mogą wymagać jedynie rozmowy. Te sprzężenia wewnętrzne to miejsca, w których szybkie i brudne struktury (np. Obiekt [], który powinien mieć trzy elementy) mogą być w porządku. W przypadku publicznego interfejsu API zwykle należy preferować coś bardziej znaczącego i wyraźnego.
Joshua Taylor
7

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.

Społeczność
źródło
2
„Nikt” jest niepoprawny pod względem faktycznym - istnieje mnóstwo narzędzi, które optymalizują pod kątem mniejszej szczegółowości, wyrażenia regularne są ekstremalnym przykładem. Ale mogłeś mieć na myśli „nikt z głównego nurtu świata Java” i dzięki temu czytaniu odpowiedź ma wiele sensu, dzięki. Moją własną troską o gadatliwość nie jest czas spędzony na pisaniu kodu, ale czas spędzony na jego czytaniu; IDE nie oszczędza czasu na czytanie; ale wydaje się, że chodzi o to, że 99% razy czyta się tylko definicję interfejsu, więc TO powinno być zwięzłe?
Michaił Ramendik
5

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:

case class Foo(duration: Int, unit: String, tooShort: Boolean)

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ę:

class TimeResult {
    public TimeResult(duration, unit, tooShort)
    public String getResult() {
        if tooShort:
           return "too short"
        else:
           return format(duration)
}

}

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.

Thanos Tintinidis
źródło
To, co chcę zrobić z czasem trwania, polega na zrobieniu ich sumy, a następnie porównaniu z innym. Brak zaangażowanych wyników. Porównanie musi brać pod uwagę zaokrąglenie, więc muszę znać oryginalne jednostki czasu w czasie porównania.
Michaił Ramendik
Prawdopodobnie byłoby możliwe stworzenie sposobu dodawania i porównywania instancji mojej klasy, ale mogłoby to być bardzo ciężkie. Zwłaszcza, że ​​muszę także podzielić i pomnożyć przez liczby zmiennoprzecinkowe (w tym interfejsie użytkownika należy sprawdzić wartości procentowe). Więc na razie stworzyłem niezmienną klasę z końcowymi polami, co wygląda na niezły kompromis.
Michaił Ramendik
Ważniejszą częścią tego konkretnego pytania była bardziej ogólna strona rzeczy i ze wszystkich odpowiedzi wydaje się, że łączy się obraz.
Michaił Ramendik
@MikhailRamendik być może jakiś akumulator byłby opcją - ale lepiej znasz ten problem. Rzeczywiście nie chodzi o rozwiązanie tego konkretnego problemu, przepraszam, jeśli zostałem nieco odsunięty na bok - moim głównym celem jest to, że język zniechęca do korzystania z „prostych” danych. czasami pomaga ci to przemyśleć swoje podejście - innym razem int jest tylko int
Thanos Tintinidis 27.04.17
@MikhailRamendik Zaletą sposobu bojlerów jest to, że kiedy masz już klasę, możesz do niej dodać zachowanie. I / lub uczyń go niezmiennym tak jak ty (jest to dobry pomysł przez większość czasu; zawsze możesz zwrócić nowy obiekt jako sumę). W końcu twoja „zbędna” klasa może obejmować wszystkie potrzebne zachowania, a wtedy koszt dla osób pobierających staje się znikomy. Ponadto możesz ich potrzebować lub nie. Zastanów się nad użyciem lombok lub autovalue .
maaartinus
5

O ile mi wiadomo, najważniejsze są następujące powody

  1. Interfejsy są podstawowym sposobem Java do abstrakcji klas wyjazdowych.
  2. Java może zwrócić tylko jedną wartość z metody - obiekt lub tablicę lub wartość rodzimą (int / long / double / float / boolean).
  3. Interfejsy nie mogą zawierać pól, tylko metody. Jeśli chcesz uzyskać dostęp do pola, musisz przejść przez metodę - stąd metody pobierające i ustawiające.
  4. Jeśli metoda zwraca interfejs, musisz mieć klasę implementującą, aby mogła zostać zwrócona.

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ą.

Thorbjørn Ravn Andersen
źródło
Na dodatek: jeśli potrzebujesz tego tylko w małym kontekście (powiedzmy, prywatna metoda w klasie), możesz użyć nagiego rekordu - idealnie niezmiennego. Ale tak naprawdę to naprawdę nie powinno przeciekać publicznego kontraktu klasy (lub, co gorsza, biblioteki!). Oczywiście możesz zdecydować się na zapisy, aby nie dopuścić do przyszłych zmian kodu; często jest to prostsze rozwiązanie.
Luaan
I zgadzam się z poglądem „Tuple nie działają dobrze w Javie”. Jeśli użyjesz leków generycznych, bardzo szybko skończy się to paskudnie.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Działają całkiem nieźle w Scali, języku o statycznym typie z rodzajami i krotkami. Więc może to wina Javy?
Andres F.
@AndresF. Zwróć uwagę na część „Jeśli używasz leków ogólnych”. Sposób, w jaki Java to robi, nie nadaje się do skomplikowanych konstrukcji, w przeciwieństwie do np. Haskell. Nie znam Scali wystarczająco dobrze, aby wykonać porównanie, ale czy to prawda, że ​​Scala pozwala na wiele wartości zwracanych? To może bardzo pomóc.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Funkcje Scala mają pojedynczą zwracaną wartość, która może być krotką (np. def f(...): (Int, Int)Jest funkcją, fktó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.
Andres F.
3

Zgadzam się z odpowiedzią JacquesB na to

Java jest (tradycyjnie) językiem OO opartym na pojedynczym paradygmacie, który optymalizuje pod kątem jawności i spójności

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 ,

Najlepszym sposobem oceny języka jest przyjrzenie się kodowi napisanemu przez jego zwolenników. „Radix enim omnium malorum est cupiditas” - a Java jest wyraźnie przykładem programowania zorientowanego na pieniądze (MOP). Jak powiedział główny orędownik Java w SGI: „Alex, musisz iść tam, gdzie są pieniądze”. Ale nie chcę szczególnie iść tam, gdzie są pieniądze - tam zwykle nie ładnie pachnie.

artem
źródło
3

(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:

  1. Dobrze, gdy twój program działa prawidłowo. Powinniśmy ułatwić pisanie programów, które postępują właściwie.
  2. Jest źle, gdy twój program robi coś złego. Powinniśmy utrudniać pisanie programów, które robią źle.

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”:

  • Gdy masz wiele baz kodu, nad którymi pracowało wiele zespołów przez wiele lat, jedną z poważnych obaw jest to, że ktoś może niewłaściwie używać API innej osoby. Funkcja wywoływana z argumentami w niewłaściwej kolejności lub wywoływana bez zapewnienia pewnych warunków / ograniczeń, których oczekuje, może być katastrofalna.
  • W specjalnym przypadku powiedz, że Twój zespół utrzymuje określony interfejs API lub bibliotekę i chciałby go zmienić lub zmienić. Im bardziej „ograniczeni” są użytkownicy w tym, w jaki sposób mogą korzystać z twojego kodu, tym łatwiej go zmienić. (Należy pamiętać, że w tym przypadku lepiej byłoby mieć rzeczywiste gwarancje, że nikt nie może używać go w nietypowy sposób).
  • Jeśli rozwój jest podzielony na wiele osób lub zespołów, dobrym pomysłem może być to, aby jedna osoba lub zespół „sprecyzował” interfejs, a inne faktycznie go wdrożyły. Aby to zadziałało, musisz być w stanie zyskać pewien stopień pewności, kiedy implementacja zostanie wykonana, że ​​implementacja faktycznie odpowiada specyfikacji.

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:

ShreevatsaR
źródło
1
Co z czytaniem kodu? Jest jeszcze jeden kompromis. Kod obciążony dziwacznymi zaklęciami i obfitymi rytuałami jest trudniejszy do odczytania; Dzięki szablonowi i niepotrzebnemu tekstowi (i hierarchiom klas!) w twoim IDE, trudniej jest zobaczyć, co tak naprawdę robi kod. Widzę argument, że kod niekoniecznie musi być łatwy do napisania (nie jestem pewien, czy się z nim zgadzam, ale ma pewne zalety), ale zdecydowanie powinien być łatwy do odczytania . Oczywiście kompleks kod może i został zbudowany z Java - byłoby głupotą twierdzić inaczej - ale to dzięki jego szczegółowość, lub pomimo tego?
Andres F.
@AndresF. Zredagowałem odpowiedź, aby była bardziej zrozumiała, nie dotyczy ona konkretnie Javy (której nie jestem wielkim fanem). Ale tak, kompromisy podczas czytania kodu: z jednej strony chcesz być w stanie łatwo odczytać „co tak naprawdę robi kod”, jak powiedziałeś. Z drugiej strony chcesz łatwo widzieć odpowiedzi na pytania takie jak: jak ten kod odnosi się do innego kodu? Co najgorsze może zrobić ten kod: jaki może mieć wpływ na inny stan, jakie są jego skutki uboczne? Od jakich czynników zewnętrznych może zależeć ten kod? (Oczywiście idealnie chcemy szybkiej odpowiedzi na oba zestawy pytań.)
ShreevatsaR
2

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.

  • W Pythonie krotka jest funkcją językową .
  • Zarówno w Javie, jak i C # krotka jest (lub byłaby) funkcją biblioteki .

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.

Berin Loritsch
źródło
4
W najnowszej wersji c # nowe krotki są obywatelami pierwszej klasy, a nie klasami z biblioteki. Dotarcie tam zajęło zbyt wiele wersji.
Igor Soloydenko
Ostatnie 3 akapity można streścić jako „Język X ma funkcję, której nie ma Java”, a ja nie widzę, co dodają do odpowiedzi. Po co wspominać C # w temacie Python to Java? Nie, po prostu nie widzę sensu. Trochę dziwne od faceta z ponad 20 000 przedstawicieli
Olivier Grégoire,
1
Jak powiedziałem: „Wspominam tylko o C #, ponieważ Tuple są funkcją biblioteki” podobną do tego, jak działałoby to w Javie, gdyby miał Tuples w głównej bibliotece. Jest bardzo niewygodny w użyciu, a lepszym rozwiązaniem jest prawie zawsze mieć dedykowaną klasę. Anonimowe obiekty drapią jednak podobnie, jak krotki. Bez wsparcia językowego dla czegoś lepszego, zasadniczo musisz pracować z tym, co jest najbardziej łatwe w utrzymaniu.
Berin Loritsch
@IgorSoloydenko, może jest nadzieja na Javę 9? To tylko jedna z tych funkcji, które są logicznym kolejnym krokiem do lambdas.
Berin Loritsch
1
@IgorSoloydenko Nie jestem pewien, czy to całkiem prawda (obywatele pierwszej klasy) - wydają się być rozszerzeniami składni do tworzenia i rozpakowywania instancji klas z biblioteki, a raczej mają wsparcie pierwszej klasy w CLR, tak jak robią to class, struct, enum.
Pete Kirkham,
0

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:

CalculateBMI(weight,height)
{
  System.out.println("BMI: " + (( weight / height ) x 703));
}

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.

CalculateBMI(Person)
{
  System.out.println("BMI: " + (( Person.weight / Person.height ) x 703));
}

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.

Pieter B.
źródło
4
Ok, ale załóżmy, że masz (hipotetyczne) zadanie, aby wyświetlić wykres: w jaki sposób BMI zależy od masy ciała dla określonej stałej wysokości. Następnie nie można tego zrobić bez utworzenia Osoby dla każdego punktu danych. Mówiąc skrajnie, nie można utworzyć instancji klasy Person bez ważnej daty urodzenia i numeru paszportu. Co teraz? Nie downvote btw, nie ważne powody do wiązania własności razem w jakiejś klasie.
artem
1
@artem to kolejny krok, w którym interfejsy wchodzą w grę. Interfejs personweightheight może być czymś takim. Dajesz się złapać w przykład, który podałem, aby stwierdzić, że to, co idzie razem, powinno być razem.
Pieter B
0

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.

Tomek
źródło
1
W szczególności narzędzie do rekrutacji foobar, z którego korzysta Google, umożliwia stosowanie dwóch języków: Java i Python. Uważam za interesujące, że Go nie wchodzi w grę. Zdarzyło mi się to podczas prezentacji w Go i zawiera ona kilka interesujących uwag na temat Javy i Pythona (a także innych języków). Na przykład wyróżnia się jeden punktor: „Być może w rezultacie programiści nie będący ekspertami mylili” łatwość użyj „z interpretacją i dynamicznym pisaniem”. Warto przeczytać.
JimmyJames
@JimmyJames właśnie doszedł do slajdu, mówiąc: „w rękach ekspertów, są wspaniali” - dobre podsumowanie problemu w pytaniu
Tom