it-swarm-eu.dev

Grokking Java Kultur - warum sind die Dinge so schwer? Wofür optimiert es?

Ich habe früher in Python viel codiert. Aus beruflichen Gründen codiere ich jetzt in Java. Die Projekte, die ich mache, sind eher klein und möglicherweise Python würde funktionieren besser, aber es gibt gültige nicht-technische Gründe für die Verwendung von Java (ich kann nicht auf Details eingehen).

Java-Syntax ist kein Problem; Es ist nur eine andere Sprache. Aber abgesehen von der Syntax hat Java eine Kultur, eine Reihe von Entwicklungsmethoden und -praktiken, die als "korrekt" angesehen werden. Und im Moment kann ich diese Kultur überhaupt nicht "groken". Also Ich würde mich sehr über Erklärungen oder Hinweise in die richtige Richtung freuen.

Ein minimales vollständiges Beispiel ist in einer von mir gestarteten Stapelüberlauffrage verfügbar: https://stackoverflow.com/questions/43619566/returning-a-result-with-several- values-the-Java-way/43620339

Ich habe eine Task-Analyse (aus einer einzelnen Zeichenfolge) und behandle einen Satz von drei Werten. In Python ist es ein Einzeiler (Tuple), in Pascal oder C ein 5-Liner-Datensatz/eine Struktur.

Den Antworten zufolge ist das Äquivalent einer Struktur in Java Syntax und ein Triple in einer weit verbreiteten Apache-Bibliothek verfügbar - die "richtige" Vorgehensweise ist jedoch das Erstellen Eine separate Klasse für den Wert, komplett mit Getter und Setter. Jemand war sehr freundlich, ein vollständiges Beispiel zu liefern. Es waren 47 Codezeilen (nun, einige dieser Zeilen waren Leerzeichen).

Ich verstehe, dass eine riesige Entwicklungsgemeinschaft wahrscheinlich nicht "falsch" ist. Das ist also ein Problem mit meinem Verständnis.

Python-Praktiken optimieren die Lesbarkeit (was in dieser Philosophie zur Wartbarkeit führt) und danach die Entwicklungsgeschwindigkeit. C-Praktiken optimieren für die Ressourcennutzung. Was tun Java-Praktiken optimieren für? Meine beste Vermutung ist Skalierbarkeit (alles sollte sich in einem Zustand befinden, der für ein Millionen-LOC-Projekt bereit ist), aber es ist eine sehr schwache Vermutung.

291

Die Java Sprache

Ich glaube, all diese Antworten verfehlen den Punkt, indem sie versuchen, der Funktionsweise von Java Absichten zuzuschreiben. Die Ausführlichkeit von Java beruht nicht darauf, dass es objektorientiert ist, da Python und viele andere Sprachen ebenfalls eine strengere Syntax haben. Die Ausführlichkeit von Java beruht auch nicht auf der Unterstützung von Zugriffsmodifikatoren. Stattdessen wurde Java einfach so entworfen und entwickelt.

Java wurde ursprünglich als leicht verbessertes C mit OO erstellt. Als solches hat Java eine Syntax aus den 70ern. Darüber hinaus ist Java sehr konservativ beim Hinzufügen von Funktionen, um die Abwärtskompatibilität beizubehalten und den Test der Zeit zu bestehen. Hätte Java 2005 trendige Funktionen wie XML-Literale hinzugefügt, als XML der letzte Schrei war, wäre die Sprache mit Geisterfunktionen aufgebläht worden, die niemanden interessieren und die ihre Entwicklung 10 Jahre später einschränken. Daher fehlt Java einfach eine Menge moderner Syntax, um Konzepte knapp auszudrücken.

Es gibt jedoch nichts Grundlegendes, das Java daran hindert, diese Syntax zu übernehmen. Zum Beispiel fügte Java 8 Lambdas und Methodenreferenzen hinzu, wodurch die Ausführlichkeit in vielen Situationen stark reduziert wurde. Java könnte in ähnlicher Weise Unterstützung für kompakte Datentypdeklarationen wie die Fallklassen von Scala hinzufügen. Aber Java hat es einfach nicht getan. Beachten Sie, dass sich benutzerdefinierte Werttypen am Horizont befinden und diese Funktion möglicherweise eine neue Syntax für deren Deklaration einführt. Ich nehme an, wir werden sehen.


Die Java Kultur

Die Geschichte der Unternehmensentwicklung Java hat uns weitgehend zu der Kultur geführt, die wir heute sehen. In den späten 90ern/frühen 00ern wurde Java zu einer äußerst beliebten Sprache für serverseitige Geschäftsanwendungen. Damals wurden diese Anwendungen größtenteils ad-hoc geschrieben und enthielten viele komplexe Aspekte wie HTTP-APIs, Datenbanken und die Verarbeitung von XML-Feeds.

In den 00er Jahren wurde klar, dass viele dieser Anwendungen viele Gemeinsamkeiten hatten und Frameworks zur Verwaltung dieser Probleme, wie das Hibernate ORM, der Xerces XML-Parser, JSPs und die Servlet-API sowie EJB, populär wurden. Diese Frameworks reduzierten zwar den Aufwand für die Arbeit in der von ihnen zu automatisierenden Domäne, erforderten jedoch Konfiguration und Koordination. Zu dieser Zeit war es aus irgendeinem Grund beliebt, Frameworks zu schreiben, um dem komplexesten Anwendungsfall gerecht zu werden, und daher war die Einrichtung und Integration dieser Bibliotheken kompliziert. Und im Laufe der Zeit wurden sie immer komplexer, da sie Merkmale ansammelten. Java In der Unternehmensentwicklung ging es nach und nach mehr darum, Bibliotheken von Drittanbietern zusammenzufügen, und weniger darum, Algorithmen zu schreiben.

Schließlich wurde die mühsame Konfiguration und Verwaltung von Unternehmenstools so schmerzhaft, dass Frameworks, insbesondere das Spring-Framework, zur Verwaltung des Managements hinzukamen. Sie könnten Ihre gesamte Konfiguration an einem Ort platzieren, so die Theorie, und das Konfigurationstool würde dann die Teile konfigurieren und sie miteinander verbinden. Leider sind diese "Framework Frameworks" mehr Abstraktion und Komplexität hinzugefügt auf der gesamten Wachskugel.

In den letzten Jahren sind immer leichtere Bibliotheken immer beliebter geworden. Nichtsdestotrotz wurde eine ganze Generation von Java Programmierern während des Wachstums schwerer Unternehmens-Frameworks erwachsen. Ihre Vorbilder, die Entwickler der Frameworks, schrieben Fabrikfabriken und Bean-Loader für die Proxy-Konfiguration. Sie mussten diese Monstrositäten täglich konfigurieren und integrieren. Infolgedessen folgte die Kultur der gesamten Gemeinschaft dem Beispiel dieser Rahmenbedingungen und neigte dazu, stark zu überentwickeln.

237

Ich glaube, ich habe eine Antwort auf einen der Punkte, die Sie ansprechen und die von anderen nicht angesprochen wurden.

Java optimiert die Erkennung von Programmierfehlern zur Kompilierungszeit, indem niemals Annahmen getroffen werden

Im Allgemeinen neigt Java dazu, Fakten über den Quellcode erst abzuleiten, nachdem der Programmierer seine Absicht bereits explizit zum Ausdruck gebracht hat. Der Compiler Java macht niemals Annahmen über das Code und verwendet nur Inferenz, um redundanten Code zu reduzieren.

Der Grund für diese Philosophie ist, dass der Programmierer nur ein Mensch ist. Was wir schreiben, ist nicht immer das, was wir mit dem Programm eigentlich vorhaben. Die Sprache Java versucht, einige dieser Probleme zu beheben, indem Entwickler gezwungen werden, ihre Typen immer explizit zu deklarieren. Dies ist nur eine Möglichkeit, doppelt zu überprüfen, ob der geschriebene Code tatsächlich das tut, was beabsichtigt war.

Einige andere Sprachen Verschieben Sie diese Logik noch weiter, indem Sie Vorbedingungen, Nachbedingungen und Invarianten überprüfen (obwohl ich nicht sicher bin, ob sie dies zur Kompilierungszeit tun). Dies sind noch extremere Möglichkeiten für den Programmierer, den Compiler seine eigene Arbeit überprüfen zu lassen.

In Ihrem Fall bedeutet dies, dass Sie dem Compiler diese Informationen zur Verfügung stellen müssen, damit der Compiler garantiert, dass Sie tatsächlich die Typen zurückgeben, von denen Sie glauben, dass Sie sie zurückgeben.

In Java gibt es zwei Möglichkeiten, dies zu tun:

  1. Verwenden Sie Triplet<A, B, C> Als Rückgabetyp (der eigentlich in Java.util Sein sollte, und ich kann nicht wirklich erklären, warum dies nicht der Fall ist. Zumal JDK8 Function, BiFunction, Consumer, BiConsumer, etc ... Es scheint nur, dass Pair und Triplet zumindest Sinn machen würden. Aber ich schweife ab)

  2. Erstellen Sie zu diesem Zweck Ihren eigenen Wertetyp, in dem jedes Feld ordnungsgemäß benannt und eingegeben wird.

In beiden Fällen kann der Compiler garantieren, dass Ihre Funktion die deklarierten Typen zurückgibt und dass der Aufrufer den Typ jedes zurückgegebenen Felds erkennt und diese entsprechend verwendet.

Einige Sprachen bieten gleichzeitig statische Typprüfung UND Typinferenz, aber das lässt die Tür offen für eine subtile Klasse von Typinkongruenzproblemen. Wenn der Entwickler beabsichtigte, einen Wert eines bestimmten Typs zurückzugeben, aber tatsächlich einen anderen zurückgibt und der Compiler STILL den Code akzeptiert, weil es vorkommt, dass sowohl die Funktion als auch der Aufrufer zufällig Methoden verwenden, die auf beide beabsichtigten angewendet werden können und die tatsächlichen Typen.

Betrachten Sie so etwas in TypeScript (oder Flow-Typ), wo Typinferenz anstelle expliziter Typisierung verwendet wird.

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);

Dies ist natürlich ein alberner Trivialfall, aber er kann viel subtiler sein und zu schwer zu debuggenden Problemen führen, da der Code richtig aussieht . Diese Art von Problemen wird in Java vollständig vermieden, und darum geht es in der gesamten Sprache.


Beachten Sie, dass nach den richtigen objektorientierten Prinzipien und der domänengesteuerten Modellierung der Fall der Daueranalyse in der verknüpften Frage auch als Java.time.Duration - Objekt zurückgegeben werden kann, was viel expliziter wäre als in beiden oben genannten Fällen.

74
LordOfThePigs

Java und Python sind die beiden Sprachen, die ich am häufigsten benutze, aber ich komme aus der anderen Richtung. Das heißt, ich war tief in der Java Welt zuvor) Ich habe angefangen, Python] zu verwenden, damit ich vielleicht helfen kann. Ich denke, die Antwort auf die größere Frage "Warum sind die Dinge so schwer" besteht aus zwei Dingen:

  • Die Kosten für die Entwicklung zwischen den beiden sind wie die Luft in einem langen Ballon Tierballon. Sie können einen Teil des Ballons drücken und ein anderer Teil schwillt an. Python neigt dazu, den frühen Teil zusammenzudrücken. Java drückt den späteren Teil zusammen.
  • Java fehlen noch einige Funktionen, die einen Teil dieses Gewichts entfernen würden. Java 8 hat dies stark beeinträchtigt, aber die Kultur hat die Änderungen nicht vollständig verdaut. Java könnte ein paar weitere Dinge wie yield.

Java 'optimiert' für hochwertige Software, die viele Jahre lang von großen Teams gewartet wird. Ich habe die Erfahrung gemacht, Dinge in Python] zu schreiben und sie ein Jahr später anzusehen und mich von meinem eigenen Code verwirren zu lassen. In Java kann ich mir ansehen winzige Ausschnitte aus dem Code anderer Leute und wissen sofort, was er tut. In Python können Sie das nicht wirklich tun. Es ist nicht so, dass einer besser ist, wie Sie zu erkennen scheinen, sie haben nur unterschiedliche Kosten.

In dem von Ihnen erwähnten speziellen Fall gibt es keine Tupel. Die einfache Lösung besteht darin, eine Klasse mit öffentlichen Werten zu erstellen. Als Java zum ersten Mal herauskam, haben die Leute dies ziemlich regelmäßig gemacht. Das erste Problem dabei ist, dass es Wartungsprobleme gibt. Wenn Sie etwas Logik oder Thread-Sicherheit hinzufügen müssen oder Polymorphismus verwenden möchten, Sie müssen zumindest jede Klasse berühren, die mit diesem 'Tupel-ähnlichen' Objekt interagiert. In Python gibt es dafür Lösungen wie __getattr__ etc. also ist es nicht so schlimm.

Es gibt jedoch einige schlechte Gewohnheiten (IMO). In diesem Fall, wenn Sie ein Tupel wollen, frage ich mich, warum Sie es zu einem veränderlichen Objekt machen würden. Sie sollten nur Getter benötigen (nebenbei bemerkt, ich hasse die get/set-Konvention, aber es ist das, was sie ist.) Ich denke, eine bloße Klasse (veränderlich oder nicht veränderbar) kann in einem privaten oder paketprivaten Kontext in Java nützlich sein . Das heißt, indem Sie die Verweise im Projekt auf die Klasse beschränken, können Sie später nach Bedarf umgestalten, ohne die öffentliche Schnittstelle der Klasse zu ändern. Hier ist ein Beispiel, wie Sie ein einfaches unveränderliches Objekt erstellen können:

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;
  }
}

Dies ist eine Art Muster, das ich benutze. Wenn Sie keine IDE verwenden, sollten Sie beginnen. Es werden die Getter (und Setter, wenn Sie sie brauchen) für Sie generiert, so dass dies nicht so schmerzhaft ist.

Ich würde mich unwohl fühlen, wenn ich nicht darauf hinweisen würde, dass es bereits einen Typ gibt, der die meisten Ihrer Bedürfnisse zu erfüllen scheint hier . Abgesehen davon ist der von Ihnen verwendete Ansatz nicht gut geeignet für Java, da er seine Schwächen und nicht seine Stärken ausspielt. Hier ist eine einfache Verbesserung:

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;
  }
}
50
JimmyJames

Bevor Sie zu wütend auf Java werden, lesen Sie bitte meine Antwort in Ihrem anderen Beitrag .

Eine Ihrer Beschwerden ist die Notwendigkeit, eine Klasse zu erstellen, um einige Werte als Antwort zurückzugeben. Dies ist ein berechtigtes Anliegen, das meiner Meinung nach zeigt, dass Ihre Programmierintuitionen auf dem richtigen Weg sind! Ich denke jedoch, dass andere Antworten die Marke verfehlen, indem sie an dem primitiven Obsession-Anti-Muster festhalten, dem Sie sich verpflichtet haben. Und Java hat nicht die gleiche Leichtigkeit, mit mehreren Grundelementen zu arbeiten wie Python hat, wo Sie mehrere Werte nativ zurückgeben und sie einfach mehreren Variablen zuweisen können .

Aber sobald Sie darüber nachdenken, was ein ApproximateDuration -Typ für Sie tut, stellen Sie fest, dass er nicht so eng auf "nur eine scheinbar unnötige Klasse, um drei Werte zurückzugeben" beschränkt ist. Das von dieser Klasse dargestellte Konzept ist tatsächlich eines Ihrer Kerndomänen-Geschäftskonzepte - die Notwendigkeit, Zeiten ungefähr darzustellen und zu vergleichen . Dies muss Teil der allgegenwärtigen Sprache des Kerns Ihrer Anwendung sein und eine gute Objekt- und Domänenunterstützung bieten, damit sie getestet, modular, wiederverwendbar und nützlich sein kann.

Ist Ihr Code, der die ungefähre Dauer zusammenfasst (oder die Dauer mit einer Fehlerquote, wie auch immer Sie dies darstellen), vollständig prozedural oder ist er objektiv? Ich würde vorschlagen, dass ein gutes Design, bei dem die ungefähre Dauer zusammen summiert wird, dies außerhalb eines konsumierenden Codes innerhalb einer Klasse vorschreibt, die selbst getestet werden kann. Ich denke, dass die Verwendung dieser Art von Domänenobjekt einen positiven Welleneffekt in Ihrem Code hat, der Sie von zeilenweisen Verfahrensschritten zur Ausführung einer einzelnen übergeordneten Aufgabe (wenn auch mit vielen Verantwortlichkeiten) hin zu Klassen mit einer einzelnen Verantwortung ablenkt das sind frei von Konflikten unterschiedlicher Anliegen.

Angenommen, Sie erfahren mehr darüber, welche Genauigkeit oder Skalierung tatsächlich erforderlich ist, damit Ihre Durationssummierung und Ihr Vergleich korrekt funktionieren, und Sie stellen fest, dass Sie ein Zwischenflag benötigen, um "ungefähr 32 Millisekunden Fehler" (in der Nähe des Quadrats) anzuzeigen Wurzel von 1000, also halb logarithmisch zwischen 1 und 1000). Wenn Sie sich an Code gebunden haben, der Primitive verwendet, um dies darzustellen, müssen Sie jede Stelle im Code finden, an der Sie is_in_seconds,is_under_1ms Haben, und ihn in is_in_seconds,is_about_32_ms,is_under_1ms Ändern. Alles müsste sich überall ändern! Wenn Sie eine Klasse erstellen, deren Aufgabe es ist, die Fehlerquote aufzuzeichnen, damit sie an anderer Stelle verwendet werden kann, müssen Ihre Verbraucher nicht wissen, welche Fehlergrenzen wichtig sind oder wie sie kombiniert werden, und sie können lediglich die relevante Fehlerquote angeben in dem Augenblick. (Das heißt, kein konsumierender Code, dessen Fehlergrenze korrekt ist, muss sich ändern, wenn Sie der Klasse eine neue Fehlergrenze hinzufügen, da alle alten Fehlergrenzen noch gültig sind.).

Schlusserklärung

Die Beschwerde über Javas Schwere scheint dann zu verschwinden, wenn Sie sich den Prinzipien von SOLID und GRASP und fortgeschrittener Softwareentwicklung) nähern.

Nachtrag

Ich werde völlig unentgeltlich und unfair hinzufügen, dass die automatischen Eigenschaften von C # und die Fähigkeit, Nur-Get-Eigenschaften in Konstruktoren zuzuweisen, dazu beitragen, den etwas chaotischen Code, den "der Java Weg") erfordert (mit expliziten), noch weiter zu bereinigen private Hintergrundfelder und Getter/Setter-Funktionen):

// 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; }
}

Hier ist eine Java Implementierung des oben genannten:

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;
  }
}

Das ist verdammt sauber. Beachten Sie die sehr wichtige und absichtliche Verwendung der Unveränderlichkeit - dies scheint für diese besondere Art von werttragender Klasse von entscheidender Bedeutung zu sein.

Für diese Angelegenheit ist diese Klasse auch ein anständiger Kandidat dafür, ein struct, ein Werttyp zu sein. Einige Tests zeigen, ob das Wechseln zu einer Struktur einen Leistungsvorteil zur Laufzeit hat (dies könnte der Fall sein).

41
ErikE

Beide Python und Java) sind gemäß der Philosophie ihrer Designer auf Wartbarkeit optimiert, haben jedoch sehr unterschiedliche Vorstellungen, wie dies erreicht werden kann.

Python ist eine Multi-Paradigmen-Sprache, die für Klarheit und Einfachheit von Code (einfach zu lesen und zu schreiben) optimiert ist.

Java ist (traditionell) eine klassenbasierte Einzelparadigma-Sprache OO), die für explizite und Konsistenz optimiert ist - auch auf Kosten von mehr ausführlicher Code.

A Python Tuple ist eine Datenstruktur mit einer festen Anzahl von Feldern. Dieselbe Funktionalität kann von einer regulären Klasse mit explizit deklarierten Feldern erreicht werden. In Python it Es ist natürlich, Tupel als Alternative zu Klassen bereitzustellen, da Sie damit den Code erheblich vereinfachen können, insbesondere aufgrund der integrierten Syntaxunterstützung für Tupel.

Dies entspricht jedoch nicht wirklich der Java -Kultur, um solche Verknüpfungen bereitzustellen, da Sie bereits explizit deklarierte Klassen verwenden können. Sie müssen keine andere Art von Datenstruktur einführen, nur um einige Codezeilen und zu speichern Vermeiden Sie einige Erklärungen.

Java bevorzugt ein einzelnes Konzept (Klassen), das konsistent mit einem Minimum an syntaktischem Zucker in Sonderfällen angewendet wird, während Python bietet mehrere Tools und viel syntaktischen Zucker, damit Sie das für einen bestimmten am besten geeignete auswählen können Zweck.

24
JacquesB

Suche nicht nach Praktiken; Es ist normalerweise eine schlechte Idee, wie in Best Practices BAD, Muster GUT? gesagt. Ich weiß, dass Sie nicht nach Best Practices fragen, aber ich denke immer noch, dass Sie dort einige relevante Elemente finden werden.

Die Suche nach einer Lösung für Ihr Problem ist besser als eine Übung, und Ihr Problem ist kein Tupel, um drei Werte in Java fast:

  • Es gibt Arrays
  • Sie können ein Array als Liste in einem Einzeiler zurückgeben: Arrays.asList (...)
  • Wenn Sie die Objektseite mit der weniger möglichen Kesselplatte (und ohne Lombok) behalten möchten:

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,....){
        ...
    }
 }

Hier haben Sie ein unveränderliches Objekt, das nur Ihre Daten enthält, und ein öffentliches Objekt, sodass keine Getter erforderlich sind. Beachten Sie jedoch, dass einige Serialisierungstools oder Persistenzschichten wie ein ORM häufig Getter/Setter verwenden (und möglicherweise einen Parameter akzeptieren, um Felder anstelle von Getter/Setter zu verwenden). Und deshalb werden diese Praktiken häufig angewendet. Wenn Sie also etwas über Praktiken wissen möchten, ist es besser zu verstehen, warum sie hier sind, um sie besser nutzen zu können.

Endlich: Ich benutze Getter, weil ich viele Serialisierungswerkzeuge benutze, aber ich schreibe sie auch nicht; Ich benutze Lombok: Ich benutze die von meiner IDE bereitgestellten Verknüpfungen.

16
Walfrat

Über Java Redewendungen im Allgemeinen:

Es gibt verschiedene Gründe, warum Java hat Klassen für alles. Meines Wissens nach ist der Hauptgrund:

Java sollte für Anfänger leicht zu erlernen sein. Je expliziter die Dinge sind, desto schwieriger ist es, wichtige Details zu übersehen. Es passiert weniger Magie, die für Anfänger schwer zu fassen wäre.


In Bezug auf Ihr spezifisches Beispiel lautet die Argumentationslinie für eine separate Klasse: Wenn diese drei Dinge stark genug miteinander verbunden sind, dass sie als ein Wert zurückgegeben werden, lohnt es sich, dieses "Ding" zu benennen. Wenn Sie einen Namen für eine Gruppe von Dingen einführen, die auf gemeinsame Weise strukturiert sind, müssen Sie eine Klasse definieren.

Sie können die Boilerplate mit Werkzeugen wie Lombok reduzieren:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}
11
marstato

Das Problem ist, dass Sie Äpfel mit Orangen vergleichen. Sie haben gefragt, wie Sie die Rückgabe von mehr als einem einzelnen Wert simulieren können, indem Sie ein schnelles und schmutziges python Beispiel mit untypisiertem Tupel) erhalten haben, und Sie haben tatsächlich die praktisch Einzeiler-Antwort erhalten.

Die akzeptierte Antwort bietet eine korrekte Geschäftslösung. Keine schnelle vorübergehende Problemumgehung, die Sie wegwerfen und beim ersten Mal korrekt implementieren müssten, wenn Sie mit dem zurückgegebenen Wert etwas Praktisches tun müssten, sondern eine POJO-Klasse, die mit einer großen Anzahl von Bibliotheken kompatibel ist, einschließlich Persistenz, Serialisierung/Deserialisierung, Instrumentierung und alles Mögliche.

Das dauert auch gar nicht lange. Das einzige, was Sie schreiben müssen, sind die Felddefinitionen. Setter, Getter, HashCode und Equals können generiert werden. Ihre eigentliche Frage sollte also sein, warum die Getter und Setter nicht automatisch generiert werden, sondern ein Syntaxproblem (syntaktisches Zuckerproblem, würden einige sagen) und kein kulturelles Problem.

Und schließlich überdenken Sie und versuchen, etwas zu beschleunigen, das überhaupt nicht wichtig ist. Der Zeitaufwand für das Schreiben von DTO-Klassen ist im Vergleich zum Zeitaufwand für die Wartung und das Debuggen des Systems unbedeutend. Daher optimiert niemand für weniger Ausführlichkeit.

7
user253752

Es gibt viele Dinge, die über die Java -Kultur) gesagt werden könnten, aber ich denke, dass es in dem Fall, mit dem Sie gerade konfrontiert sind, einige wichtige Aspekte gibt:

  1. Bibliothekscode wird einmal geschrieben, aber viel häufiger verwendet. Während es schön ist, den Aufwand für das Schreiben der Bibliothek zu minimieren, lohnt es sich auf lange Sicht wahrscheinlich mehr, so zu schreiben, dass der Aufwand für die Verwendung der Bibliothek minimiert wird.
  2. Das bedeutet, dass selbstdokumentierende Typen großartig sind: Methodennamen machen deutlich, was passiert und was Sie aus einem Objekt herausholen.
  3. Die statische Typisierung ist ein sehr nützliches Werkzeug, um bestimmte Fehlerklassen zu beseitigen. Es behebt sicherlich nicht alles (die Leute scherzen gern über Haskell, dass es wahrscheinlich richtig ist, wenn das Typsystem Ihren Code akzeptiert), aber es macht es sehr einfach, bestimmte Arten von falschen Dingen unmöglich zu machen.
  4. Beim Schreiben von Bibliothekscode geht es darum, Verträge anzugeben. Durch das Definieren von Schnittstellen für Ihre Argument- und Ergebnistypen werden die Grenzen Ihrer Verträge klarer definiert. Wenn etwas ein Tupel akzeptiert oder produziert, kann man nicht sagen, ob es die Art von Tupel ist, die Sie tatsächlich erhalten oder produzieren sollten, und es gibt nur sehr wenige Einschränkungen für einen solchen generischen Typ (hat es überhaupt die richtige Anzahl von Elementen?) sie von dem Typ, den Sie erwartet hatten?).

"Struct" -Klassen mit Feldern

Wie bereits in anderen Antworten erwähnt, können Sie einfach eine Klasse mit öffentlichen Feldern verwenden. Wenn Sie diese endgültig erstellen, erhalten Sie eine unveränderliche Klasse, die Sie mit dem Konstruktor initialisieren:

   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;
      }
   }

Dies bedeutet natürlich, dass Sie an eine bestimmte Klasse gebunden sind und dass alles, was jemals ein Analyseergebnis erzeugen oder verbrauchen muss, diese Klasse verwenden muss. Für einige Anwendungen ist das in Ordnung. Für andere kann dies Schmerzen verursachen. Viel Java Code handelt vom Definieren von Verträgen, und das führt Sie normalerweise in Schnittstellen.

Eine weitere Gefahr besteht darin, dass Sie mit einem klassenbasierten Ansatz Felder verfügbar machen und alle diese Felder müssen Werte haben. Zum Beispiel müssen isSeconds und millis immer einen Wert haben, auch wenn isLessThanOneMilli wahr ist. Wie sollte der Wert des Millis-Feldes interpretiert werden, wenn isLessThanOneMilli wahr ist?

"Strukturen" als Schnittstellen

Mit den statischen Methoden, die in Schnittstellen zulässig sind, ist es relativ einfach, unveränderliche Typen ohne großen syntaktischen Aufwand zu erstellen. Zum Beispiel könnte ich die Art der Ergebnisstruktur, von der Sie sprechen, wie folgt implementieren:

   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;
            }
         };
      }
   }

Das ist immer noch eine Menge Boilerplate, da stimme ich absolut zu, aber es gibt auch ein paar Vorteile, und ich denke, diese beginnen, einige Ihrer Hauptfragen zu beantworten.

Bei einer Struktur wie diesem Analyseergebnis ist das Vertrag Ihres Parsers sehr klar definiert. In Python unterscheidet sich ein Tupel nicht wirklich von einem anderen Tupel. In Java ist statische Typisierung verfügbar, sodass bestimmte Fehlerklassen bereits ausgeschlossen sind. Wenn Sie beispielsweise ein Tupel in Python zurückgeben und das Tupel (millis, isSeconds, isLessThanOneMilli) zurückgeben möchten, können Sie versehentlich Folgendes tun:

return (true, 500, false)

als du meintest:

return (500, true, false)

Mit dieser Art von Java Schnittstelle) können Sie nicht kompilieren:

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

überhaupt. Du musst:

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

Dies ist ein Vorteil von statisch typisierten Sprachen im Allgemeinen.

Dieser Ansatz gibt Ihnen auch die Möglichkeit, die Werte einzuschränken, die Sie erhalten können. Wenn Sie beispielsweise getMillis () aufrufen, können Sie überprüfen, ob isLessThanOneMilli () wahr ist, und in diesem Fall eine IllegalStateException (zum Beispiel) auslösen, da in diesem Fall kein aussagekräftiger Wert von millis vorhanden ist.

Es schwierig machen, das Falsche zu tun

Im obigen Schnittstellenbeispiel haben Sie immer noch das Problem, dass Sie versehentlich die Argumente isSeconds und isLessThanOneMilli austauschen könnten, da sie denselben Typ haben.

In der Praxis möchten Sie möglicherweise TimeUnit und die Dauer verwenden, um ein Ergebnis wie das folgende zu erhalten:

   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();
            }
         };
      }
   }

Das wird ein viel mehr Code, aber Sie müssen ihn nur einmal schreiben und (vorausgesetzt, Sie haben die Dinge richtig dokumentiert) die Leute, die am Ende mit Ihrem Code muss nicht raten, was das Ergebnis bedeutet, und kann nicht versehentlich Dinge wie result[0] wenn sie bedeuten result[1]. Sie können immer noch ziemlich prägnant create Instanzen erstellen, und es ist auch nicht allzu schwierig, Daten aus ihnen herauszuholen:

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

Beachten Sie, dass Sie mit dem klassenbasierten Ansatz auch so etwas tun können. Geben Sie einfach Konstruktoren für die verschiedenen Fälle an. Sie haben jedoch immer noch die Frage, wie die anderen Felder initialisiert werden sollen, und Sie können den Zugriff auf sie nicht verhindern.

In einer anderen Antwort wurde erwähnt, dass die unternehmensähnliche Natur von Java bedeutet, dass Sie in den meisten Fällen andere bereits vorhandene Bibliotheken erstellen oder Bibliotheken schreiben, die von anderen Personen verwendet werden können. Ihr public API sollte nicht viel Zeit in Anspruch nehmen, um die Ergebnistypen zu entschlüsseln, wenn dies vermieden werden kann.

Sie schreiben diese Strukturen nur einmal, aber Sie erstellen sie viele Male, also möchten Sie immer noch diese prägnante Erstellung (die Sie erhalten). Die statische Eingabe stellt sicher, dass die Daten, die Sie aus ihnen erhalten, Ihren Erwartungen entsprechen.

Trotzdem gibt es immer noch Orte, an denen einfache Tupel oder Listen viel Sinn machen können. Die Rückgabe eines Arrays von etwas kann weniger Aufwand bedeuten. Wenn dies der Fall ist (und dieser Aufwand erheblich ist, den Sie mit der Profilerstellung ermitteln würden), kann die Verwendung eines einfachen Array von Werten intern dazu führen viel Sinn. Ihre öffentliche API sollte wahrscheinlich noch klar definierte Typen haben.

7
Joshua Taylor

Dies sind drei verschiedene Faktoren, die zu dem beitragen, was Sie beobachten.

Tupel gegen benannte Felder

Vielleicht das Trivialste - in den anderen Sprachen haben Sie ein Tupel verwendet. Die Debatte darüber, ob Tupel eine gute Idee sind, ist nicht wirklich der Punkt - aber in Java haben Sie eine schwerere Struktur verwendet, so dass es ein etwas unfairer Vergleich ist: Sie hätten eine Reihe von Objekten und eine Art Casting verwenden können.

Sprachsyntax

Könnte es einfacher sein, die Klasse zu deklarieren? Ich spreche nicht davon, die Felder öffentlich zu machen oder eine Karte zu verwenden, sondern von Scalas Fallklassen, die alle Vorteile des von Ihnen beschriebenen Setups bieten, aber viel prägnanter sind:

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

Wir könnten das haben - aber es gibt Kosten: Die Syntax wird komplizierter. Natürlich könnte es sich für einige Fälle oder sogar für die meisten Fälle oder sogar für die meisten Fälle für die nächsten 5 Jahre lohnen - aber es muss beurteilt werden. Übrigens ist dies eines der netten Dinge mit Sprachen, die Sie selbst ändern können (d. H. LISP) - und beachten Sie, wie dies aufgrund der Einfachheit der Syntax möglich wird. Selbst wenn Sie die Sprache nicht ändern, ermöglicht eine einfache Syntax leistungsfähigere Tools. Zum Beispiel vermisse ich oft einige Refactoring-Optionen, die für Java verfügbar sind, aber nicht für Scala.

Sprachphilosophie

Der wichtigste Faktor ist jedoch, dass eine Sprache eine bestimmte Denkweise ermöglichen sollte. Manchmal mag es sich bedrückend anfühlen (ich habe mir oft Unterstützung für eine bestimmte Funktion gewünscht), aber das Entfernen von Funktionen ist genauso wichtig wie das Vorhandensein. Könnten Sie alles unterstützen? Sicher, aber dann können Sie auch einfach einen Compiler schreiben, der jede einzelne Sprache kompiliert. Mit anderen Worten, Sie werden keine Sprache haben - Sie werden eine Obermenge von Sprachen haben und jedes Projekt würde im Grunde eine Teilmenge übernehmen.

Es ist natürlich möglich, Code zu schreiben, der gegen die Philosophie der Sprache verstößt, und wie Sie festgestellt haben, sind die Ergebnisse oft hässlich. Eine Klasse mit nur ein paar Feldern in Java zu haben, ist vergleichbar mit der Verwendung eines var in Scala, dem Entarteten von Prologprädikaten zu Funktionen, dem Ausführen einer unsicheren Ausführung in haskell usw. Java Klassen sind nicht gemeint leicht sein - sie sind nicht da, um Daten weiterzugeben. Wenn etwas schwierig erscheint, ist es oft fruchtbar, zurückzutreten und zu prüfen, ob es einen anderen Weg gibt. In Ihrem Beispiel:

Warum ist die Dauer von den Einheiten getrennt? Es gibt viele Zeitbibliotheken, mit denen Sie eine Dauer deklarieren können - etwa Dauer (5, Sekunden) (Syntax variiert), mit der Sie weitaus robuster tun können, was Sie wollen. Vielleicht möchten Sie es konvertieren - warum prüfen, ob Ergebnis [1] (oder [2]?) Eine 'Stunde' ist und mit 3600 multipliziert wird? Und für das dritte Argument - was ist ihr Zweck? Ich würde vermuten, dass Sie irgendwann "weniger als 1 ms" oder die tatsächliche Zeit drucken müssen - das ist eine Logik, die natürlich zu den Zeitdaten gehört. Das heißt, Sie sollten eine Klasse wie diese haben:

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

}}

oder was auch immer Sie tatsächlich mit den Daten tun möchten, wodurch die Logik gekapselt wird.

Natürlich könnte es einen Fall geben, in dem dieser Weg nicht funktioniert - ich sage nicht, dass dies der magische Algorithmus zum Konvertieren von Tupel-Ergebnissen in idiomatischen Java Code ist! Und es kann Fälle geben, in denen es sehr hässlich und schlecht ist und Sie vielleicht eine andere Sprache hätten verwenden sollen - deshalb gibt es doch so viele!

Meine Ansicht darüber, warum Klassen in Java "schwere Strukturen" sind, ist jedoch, dass Sie sie nicht als Datencontainer verwenden sollen, sondern als in sich geschlossene Logikzellen.

5

Nach meinem Verständnis sind die Hauptgründe

  1. Schnittstellen sind die grundlegende Java Methode, um Klassen zu abstrahieren.
  2. Java kann nur einen einzelnen Wert von einer Methode zurückgeben - ein Objekt oder ein Array oder einen nativen Wert (int/long/double/float/boolean).
  3. Schnittstellen dürfen keine Felder enthalten, sondern nur Methoden. Wenn Sie auf ein Feld zugreifen möchten, müssen Sie eine Methode durchlaufen - daher Getter und Setter.
  4. Wenn eine Methode eine Schnittstelle zurückgibt, müssen Sie über eine implementierende Klasse verfügen, um tatsächlich zurückkehren zu können.

Dies gibt Ihnen die "Sie müssen eine Klasse schreiben, um für ein nicht triviales Ergebnis zurückzukehren", was wiederum ziemlich schwer ist. Wenn Sie eine Klasse anstelle der Schnittstelle verwenden, können Sie nur Felder haben und diese direkt verwenden, aber das bindet Sie an eine bestimmte Implementierung.

(Diese Antwort ist keine Erklärung für Java im Besonderen, sondern befasst sich stattdessen mit der allgemeinen Frage „Wofür könnten [schwere] Praktiken optimiert werden?“)

Betrachten Sie diese beiden Prinzipien:

  1. Es ist gut, wenn Ihr Programm das Richtige tut. Wir sollten es einfach machen, Programme zu schreiben, die das Richtige tun.
  2. Es ist schlecht, wenn Ihr Programm das Falsche tut. Wir sollten es schwieriger machen, Programme zu schreiben, die das Falsche tun.

Der Versuch, eines dieser Ziele zu optimieren, kann manchmal dem anderen im Weg stehen (d. H. es schwieriger zu machen, das Falsche zu tun, kann es auch schwieriger machen, das Richtige zu tun oder umgekehrt).

Welche Kompromisse in einem bestimmten Fall gemacht werden, hängt von der Anwendung, den Entscheidungen der betreffenden Programmierer oder Teams und der Kultur (der Organisation oder der Sprachgemeinschaft) ab.

Wenn beispielsweise ein Fehler oder ein Ausfall von einigen Stunden in Ihrem Programm zum Verlust von Menschenleben (medizinische Systeme, Luftfahrt) oder sogar nur zu Geld führen könnte (wie beispielsweise Millionen von Dollar in den Anzeigensystemen von Google), würden Sie andere Kompromisse eingehen (nicht nur in Ihrer Sprache, aber auch in anderen Aspekten der Ingenieurkultur) als bei einem einmaligen Skript: Es ist wahrscheinlich eher "schwer".

Andere Beispiele, die dazu neigen, Ihr System "schwerer" zu machen:

  • Wenn Sie eine große Codebasis haben, an der viele Teams über viele Jahre hinweg gearbeitet haben, besteht ein großes Problem darin, dass jemand die API eines anderen möglicherweise falsch verwendet. Eine Funktion, die mit Argumenten in der falschen Reihenfolge aufgerufen wird oder ohne die erwarteten Voraussetzungen/Einschränkungen aufgerufen wird, kann katastrophal sein.
  • Angenommen, Ihr Team verwaltet eine bestimmte API oder Bibliothek und möchte diese ändern oder umgestalten. Je „eingeschränkter“ Ihre Benutzer sind, wie sie Ihren Code verwenden könnten, desto einfacher ist es, ihn zu ändern. (Beachten Sie, dass es hier vorzuziehen wäre, hier tatsächlich zu garantieren, dass niemand es auf ungewöhnliche Weise verwenden könnte.)
  • Wenn die Entwicklung auf mehrere Personen oder Teams aufgeteilt ist, scheint es eine gute Idee zu sein, dass eine Person oder ein Team die Schnittstelle „spezifiziert“ und andere sie tatsächlich implementieren. Damit dies funktioniert, müssen Sie in der Lage sein, nach Abschluss der Implementierung ein gewisses Maß an Vertrauen zu gewinnen, dass die Implementierung tatsächlich der Spezifikation entspricht.

Dies sind nur einige Beispiele, um Ihnen eine Vorstellung von Fällen zu geben, in denen es wirklich beabsichtigt sein kann, Dinge „schwer“ zu machen (und es für Sie schwieriger zu machen, nur schnell Code zu schreiben). (Man könnte sogar argumentieren, dass das Schreiben von Code, wenn es viel Aufwand erfordert, dazu führen kann, dass Sie vor dem Schreiben von Code genauer überlegen! Natürlich wird diese Argumentation schnell lächerlich.)

Ein Beispiel: Googles internes Python -System macht die Dinge in der Regel „schwer“, sodass Sie nicht einfach den Code eines anderen importieren können. Sie müssen die Abhängigkeit in einer BUILD-Datei deklarieren , das Team, dessen Code Sie importieren möchten, muss seine Bibliothek als für Ihren Code sichtbar deklariert haben usw.


Anmerkung: Bei alledem geht es nur darum, wann die Dinge "schwer" werden. Ich behaupte absolut nicht, dass Java oder Python (entweder die Sprachen selbst oder ihre Kulturen) optimale Kompromisse für eine bestimmte Person eingehen) Fall, darüber müssen Sie nachdenken. Zwei verwandte Links zu solchen Kompromissen:

3
ShreevatsaR

Ich bin damit einverstanden, dass JacquesB darauf antwortet

Java ist (traditionell) eine klassenbasierte Einzelparadigma-Sprache OO), die hinsichtlich ihrer Aussagekraft und Konsistenz optimiert wird

Explizite Aussagen und Beständigkeit sind jedoch nicht die Endziele, für die eine Optimierung erforderlich ist. Wenn Sie sagen, "Python ist auf Lesbarkeit optimiert", erwähnen Sie sofort, dass das Endziel "Wartbarkeit" und "Entwicklungsgeschwindigkeit" ist.

Was erreichen Sie, wenn Sie explizite und konsistente Ergebnisse erzielt haben? Java Weg? Ich gehe davon aus, dass es sich um eine Sprache handelt, die behauptet, vorhersehbare, konsistente und einheitliche Wege zur Lösung von Softwareproblemen zu bieten.

Mit anderen Worten, Java Kultur ist optimiert, damit Manager glauben, dass sie die Softwareentwicklung verstehen.

Oder, wie ein weiser Kerl hat es vor sehr langer Zeit ausgedrückt ,

Der beste Weg, eine Sprache zu beurteilen, besteht darin, sich den von ihren Befürwortern geschriebenen Code anzusehen. "Radix enim omnium malorum est cupiditas" - und Java ist eindeutig ein Beispiel für eine geldorientierte Programmierung (MOP). Als Hauptvertreter von Java bei SGI) sagte mir: "Alex, du musst dorthin gehen, wo das Geld ist." Aber ich möchte nicht besonders dorthin gehen, wo das Geld ist - es riecht dort normalerweise nicht gut.

3
artem

Die Java -Kultur hat sich im Laufe der Zeit mit starken Einflüssen sowohl von Open Source- als auch von Unternehmenssoftware-Hintergründen weiterentwickelt - was eine seltsame Mischung ist, wenn Sie wirklich darüber nachdenken. Unternehmenslösungen erfordern umfangreiche Tools und Open Source erfordert Einfachheit. Das Endergebnis ist, dass Java irgendwo in der Mitte ist.

Ein Teil dessen, was die Empfehlung beeinflusst, ist das, was in Python und Java ist sehr unterschiedlich) als lesbar und wartbar angesehen wird.

  • In Python ist das Tupel eine Sprachfunktion .
  • In beiden Java und C #) ist (oder wäre) das Tupel eine Bibliotheksfunktion .

Ich erwähne C # nur, weil die Standardbibliothek eine Reihe von Tupelklassen <A, B, C, .. n> enthält und ein perfektes Beispiel dafür ist, wie unhandlich Tupel sind, wenn die Sprache sie nicht direkt unterstützt. In fast allen Fällen wird Ihr Code lesbarer und wartbarer, wenn Sie gut ausgewählte Klassen ausgewählt haben, um das Problem zu lösen. In dem speziellen Beispiel in Ihrer Frage zum verknüpften Stapelüberlauf werden die anderen Werte leicht als berechnete Getter für das Rückgabeobjekt ausgedrückt.

Eine interessante Lösung, die die C # -Plattform gemacht hat und die einen glücklichen Mittelweg bietet, ist die Idee von anonymen Objekten (veröffentlicht in C # 3. ), die diesen Juckreiz ziemlich gut kratzen. Leider hat Java hat noch kein Äquivalent.

Bis die Sprachfunktionen von Java geändert werden, besteht die am besten lesbare und wartbare Lösung darin, ein dediziertes Objekt zu haben. Dies ist auf Einschränkungen in der Sprache zurückzuführen, die bis zu ihren Anfängen im Jahr 1995 zurückreichen. Die ursprünglichen Autoren hatten viel mehr Sprachfunktionen geplant, die es nie geschafft haben, und die Abwärtskompatibilität ist eine der Hauptbeschränkungen im Zusammenhang mit der Entwicklung von Java im Laufe der Zeit.

2
Berin Loritsch

Um ehrlich zu sein, ist die Kultur, dass Java Programmierer ursprünglich dazu neigten, von Universitäten zu kommen, an denen objektorientierte Prinzipien und Prinzipien für nachhaltiges Software-Design gelehrt wurden.

Wie ErikE in seiner Antwort ausführlicher sagt, scheinen Sie keinen nachhaltigen Code zu schreiben. Was ich aus Ihrem Beispiel sehe, ist eine sehr unangenehme Verstrickung von Bedenken.

In der Kultur von Java) wissen Sie in der Regel, welche Bibliotheken verfügbar sind, und können so viel mehr erreichen als Ihre Programmierung ohne Manschette. Sie würden also Ihre Eigenheiten austauschen für die Designmuster und -stile, die sich in industriellen Hardcore-Umgebungen bewährt haben.

Aber wie Sie sagen, ist dies nicht ohne Nachteile: Nachdem ich heute über 10 Jahre lang Java] verwendet habe, verwende ich entweder Node/Javascript oder Go für neue Projekte, da beide eine schnellere Entwicklung ermöglichen Bei Architekturen im Microservice-Stil reichen diese häufig aus. Gemessen an der Tatsache, dass Google zuerst stark Java verwendet hat, aber der Urheber von Go war, denke ich, dass sie möglicherweise dasselbe tun. Aber obwohl ich jetzt Go und Javascript verwende, Ich benutze immer noch viele der Designfähigkeiten, die ich durch jahrelange Verwendung und Verständnis von Java erworben habe.

0
Tom

Ich denke, eines der wichtigsten Dinge bei der Verwendung einer Klasse in diesem Fall ist, dass das, was zusammen gehört, zusammen bleiben sollte.

Ich habe diese Diskussion umgekehrt über Methodenargumente geführt: Betrachten Sie eine einfache Methode, die den BMI berechnet:

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

In diesem Fall würde ich gegen diesen Stil argumentieren, weil Gewicht und Größe zusammenhängen. Die Methode "kommuniziert", das sind zwei separate Werte, wenn dies nicht der Fall ist. Wann würden Sie einen BMI mit dem Gewicht einer Person und der Größe einer anderen Person berechnen? Es würde keinen Sinn ergeben.

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

Das macht viel mehr Sinn, weil Sie jetzt klar kommunizieren, dass Größe und Gewicht aus derselben Quelle stammen.

Gleiches gilt für die Rückgabe mehrerer Werte. Wenn sie eindeutig verbunden sind, geben Sie ein hübsches kleines Paket zurück und verwenden Sie ein Objekt, wenn sie nicht mehrere Werte zurückgeben.

0
Pieter B