Grokking Java Culture - warum sind die Dinge so schwer? Wofür optimiert es? [geschlossen]


288

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

Die Java-Syntax ist kein Problem. es ist nur eine andere Sprache. Abgesehen von der Syntax verfügt Java jedoch über eine Kultur, eine Reihe von Entwicklungsmethoden und -praktiken, die als "korrekt" angesehen werden. Und im Moment kann ich diese Kultur überhaupt nicht "ausgraben". Deshalb würde ich mich über Erklärungen oder Hinweise in die richtige Richtung sehr freuen.

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

Ich habe eine Aufgabe - analysieren (aus einer einzelnen Zeichenfolge) und behandeln einen Satz von drei Werten. In Python ist es ein Einzeiler (Tupel), in Pascal oder C ein 5-Zeilen-Datensatz / Struktur.

Den Antworten zufolge ist das Äquivalent einer Struktur in der Java-Syntax und ein Tripel in einer weit verbreiteten Apache-Bibliothek verfügbar. Die "richtige" Methode besteht jedoch darin, eine separate Klasse für den Wert einschließlich zu erstellen Getter und Setter. Jemand war sehr freundlich, ein vollständiges Beispiel zu liefern. Es waren 47 Codezeilen (einige dieser Zeilen waren Leerzeichen).

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

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


1
Ich werde die technischen Aspekte der Frage nicht beantworten, aber immer noch im Kontext: softwareengineering.stackexchange.com/a/63145/13899
Newtopian

74
Sie können Steve Yegges Aufsatz Hinrichtung im Königreich der Nomen
Colonel Panic am


3
Hier ist, was ich denke, die richtige Antwort. Sie machen Java nicht zu schaffen, weil Sie daran denken, wie ein Sysadmin zu programmieren, nicht wie ein Programmierer. Software ist für Sie etwas, mit dem Sie eine bestimmte Aufgabe auf die zweckmäßigste Weise lösen. Ich schreibe seit 20 Jahren Java-Code und einige der Projekte, an denen ich gearbeitet habe, haben ein Team von 20, 2 Jahren in Anspruch genommen. Java ist kein Ersatz für Python und auch nicht umgekehrt. Sie machen beide ihre Arbeit, aber ihre Arbeit ist ganz anders.
Richard

1
Der Grund, warum Java der De-facto-Standard ist, ist, dass es einfach richtig ist. Es wurde zum ersten Mal von ernsthaften Leuten geschaffen, die Bärte hatten, bevor Bärte in Mode waren. Wenn Sie es gewohnt sind, Shell-Skripte zum Manipulieren von Dateien zu aktivieren, scheint Java unerträglich aufgebläht zu sein. Wenn Sie jedoch einen doppelt redundanten Servercluster erstellen möchten, der 50 Millionen Menschen pro Tag bedienen kann, der durch Redis-Caching-Cluster, drei Abrechnungssysteme und einen 20-Millionen-Pfund-Oracle-Datenbankcluster unterstützt wird Die Datenbank in Python enthält 25 Zeilen weniger Code.
Richard

Antworten:


237

Die Java-Sprache

Ich glaube, dass all diese Antworten den Sinn verfehlen, wenn man versucht, die Funktionsweise von Java mit Absicht zu beschreiben. Javas Ausführlichkeit beruht nicht darauf, dass es objektorientiert ist, da Python und viele andere Sprachen ebenfalls eine tersere Syntax haben. Javas Ausführlichkeit beruht auch nicht auf der Unterstützung von Zugriffsmodifikatoren. Stattdessen ist es einfach so, wie Java entworfen wurde und sich weiterentwickelt hat.

Java wurde ursprünglich als leicht verbessertes C mit OO erstellt. Als solches hat Java eine 70er-Ära-Syntax. Darüber hinaus ist Java sehr konservativ in Bezug auf das Hinzufügen von Funktionen, um die Abwärtskompatibilität beizubehalten und die Zeit zu überdauern. Hätte Java im Jahr 2005 trendige Funktionen wie XML-Literale hinzugefügt, als XML in aller Munde war, wäre die Sprache mit Geisterfunktionen überfüllt gewesen, die niemanden interessieren und deren 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 davon abhält, diese Syntax zu übernehmen. Beispielsweise fügte Java 8 Lambdas und Methodenverweise hinzu, wodurch die Ausführlichkeit in vielen Situationen erheblich reduziert wurde. Java könnte auf ähnliche Weise die Unterstützung für kompakte Datentypdeklarationen wie die Fallklassen von Scala hinzufügen. Aber Java hat es einfach nicht getan. Beachten Sie, dass benutzerdefinierte Werttypen in Sicht sind und diese Funktion möglicherweise eine neue Syntax für ihre Deklaration einführt. Ich nehme an, wir werden sehen.


Die Java-Kultur

Die Geschichte der Enterprise Java-Entwicklung 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 weitgehend ad-hoc geschrieben und beinhalteten viele komplexe Aspekte wie HTTP-APIs, Datenbanken und die Verarbeitung von XML-Feeds.

In den 00er Jahren stellte sich heraus, dass viele dieser Anwendungen viele Gemeinsamkeiten aufwiesen und Frameworks zur Verwaltung dieser Probleme, wie der 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 bestimmten Domäne, die sie für die Automatisierung festlegen, erforderten jedoch Konfiguration und Koordination. Aus irgendeinem Grund war es damals populär, Frameworks zu schreiben, um den komplexesten Anwendungsfall abzudecken, und daher waren diese Bibliotheken kompliziert einzurichten und zu integrieren. Und im Laufe der Zeit wurden sie immer komplexer, je mehr Merkmale sie anhäuften. Bei der Java-Unternehmensentwicklung ging es zunehmend 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, die Verwaltung übernahmen. Sie könnten Ihre gesamte Konfiguration an einem Ort ablegen, laut Theorie, und das Konfigurationstool würde dann die Teile konfigurieren und sie miteinander verbinden. Leider haben diese "Framework Frameworks" mehr Abstraktion und Komplexität über die gesamte Wachskugel hinzugefügt .

In den letzten Jahren haben leichtgewichtigere Bibliotheken an Popularität gewonnen. Nichtsdestotrotz wurde eine ganze Generation von Java-Programmierern während des Wachstums von Frameworks für Großunternehmen erwachsen. Ihre Vorbilder, die Entwickler der Frameworks, schrieben Factory-Factorys und Proxy-Konfigurations-Bean-Loader. Sie mussten diese Monstrositäten von Tag zu Tag konfigurieren und integrieren. Infolgedessen folgte die Kultur der gesamten Gemeinschaft dem Beispiel dieser Rahmenbedingungen und neigte dazu, die Ingenieure stark zu übertreiben.


15
Was lustig ist, ist, dass andere Sprachen einige "Java-Ismen" aufzugreifen scheinen. Die Funktionen von PHP OO sind stark von Java inspiriert. Heutzutage wird JavaScript häufig wegen seiner Vielzahl von Frameworks und der Schwierigkeit, alle Abhängigkeiten zu erfassen und eine neue Anwendung zu starten, kritisiert.
Marcus

14
@marcus vielleicht, weil manche Leute endlich gelernt haben, das Rad nicht neu zu erfinden? Abhängigkeiten sind die Kosten dafür, das Rad nicht neu zu erfinden
Walfrat

9
"Terse" bedeutet oft arkane, "clevere" , Code-Golf-artige Einzeiler.
Tulains Córdova

7
Nachdenkliche, gut geschriebene Antwort. Historischer Zusammenhang. Relativ uneinsichtig. Schön gemacht.
Cheezmeister

10
@ TulainsCórdova Ich stimme zu, dass wir "kluge Tricks wie die Pest vermeiden" sollten (Donald Knuth), aber das bedeutet nicht, eine forSchleife zu schreiben , wenn ein mapbesser geeignet (und lesbarer) wäre. Es gibt ein fröhliches Medium.
Brian McCutchon

73

Ich glaube, ich habe eine Antwort auf einen der Punkte, die Sie angesprochen haben und die andere nicht angesprochen haben.

Java optimiert die Erkennung von Programmierfehlern während der Kompilierung, indem es keine Annahmen trifft

Im Allgemeinen lässt Java Tatsachen über den Quellcode nur dann ableiten, wenn der Programmierer seine Absicht bereits ausdrücklich zum Ausdruck gebracht hat. Der Java-Compiler trifft keine Annahmen über den 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 eigentlich mit dem Programm vorhaben. Die Java-Sprache versucht, einige dieser Probleme zu beheben, indem sie den Entwickler dazu zwingt, ihre Typen immer explizit zu deklarieren. Dies ist nur ein Weg, um zu überprüfen, ob der geschriebene Code tatsächlich das tut, was beabsichtigt war.

Einige andere Sprachen treiben diese Logik noch weiter voran, indem sie Vorbedingungen, Nachbedingungen und Invarianten prü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 der Compiler diese Informationen bereitstellen muss, 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 (was wirklich in sein sollte java.util, und ich kann nicht wirklich erklären , warum es nicht ist. Vor allem , da JDK8 einführen Function, BiFunction, Consumer, BiConsumer, etc ... Es scheint nur , dass Pairund Tripletzumindest Sinn machen würde. Aber ich schweife ab )

  2. Erstellen Sie zu diesem Zweck einen 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 eine statische Typprüfung UND eine Typinferenz, aber dies 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 den Code NOCH akzeptiert, weil zufällig sowohl die Funktion als auch der Aufrufer nur Methoden verwenden, die auf beide beabsichtigten angewendet werden können und die tatsächlichen Typen.

Betrachten Sie so etwas in Typescript (oder Flusstyp), wo die Typinferenz anstelle der expliziten 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 dummer, trivialer Fall, aber er kann viel subtiler sein und zu schwierig zu debuggenden Problemen führen, da der Code richtig aussieht . Diese Art von Problemen wird in Java vollständig vermieden, und genau darum geht es in der gesamten Sprache.


Beachten Sie, dass gemäß den ordnungsgemäßen objektorientierten Prinzipien und der domänengesteuerten Modellierung der Fall der Analyse der Dauer in der verknüpften Frage auch als java.time.DurationObjekt zurückgegeben werden kann, was viel expliziter wäre als die beiden oben genannten Fälle.


38
Zu behaupten, dass Java für die Code-Korrektheit optimiert, mag ein gültiger Punkt sein, aber es scheint mir (beim Programmieren vieler Sprachen, in letzter Zeit ein bisschen Java), dass die Sprache dabei entweder versagt oder extrem ineffizient ist. Zugegeben, Java ist alt und viele Dinge gab es damals noch nicht, aber es gibt moderne Alternativen, die eine wohl bessere Korrektheitsprüfung bieten, wie Haskell (und ich wage es zu sagen? Rust or Go). Die Unhandlichkeit von Java ist für dieses Ziel unnötig. - Und außerdem ist dies nicht überhaupt die Kultur erklären, aber Solomonoff ergab , dass die Kultur BS sowieso.
Tomsmeding

8
Dieses Beispiel zeigt nicht, dass die Typinferenz das Problem ist, nur dass die Entscheidung von JavaScript, implizite Konvertierungen in einer dynamischen Sprache zuzulassen, dumm ist. Jede vernünftige dynamische Sprache oder statisch angepriesene Sprache mit Typfehlern würde dies nicht zulassen. Als Beispiel für beide Arten: Python würde eine Laufzeitausnahme auslösen, Haskell würde nicht kompilieren.
Voo

39
@tomsmeding Es ist erwähnenswert, dass dieses Argument besonders albern ist, da Haskell schon 5 Jahre länger als Java existiert. Java verfügt möglicherweise über ein Typensystem, das jedoch bei Veröffentlichung noch nicht einmal auf dem neuesten Stand der Technik war.
Alexis King

21
"Java optimiert für die Erkennung von Kompilierungsfehlern" - Nein. Es stellt es bereit , aber, wie andere angemerkt haben, optimiert es es sicherlich nicht im wahrsten Sinne des Wortes. Darüber hinaus hat dieses Argument nichts mit der Frage von OP zu tun, da andere Sprachen, die tatsächlich für die Erkennung von Kompilierungsfehlern optimiert sind, für ähnliche Szenarien eine wesentlich einfachere Syntax aufweisen. Die übertriebene Ausführlichkeit von Java hat genau nichts mit der Überprüfung während der Kompilierung zu tun.
Konrad Rudolph

19
@KonradRudolph Ja, diese Antwort ist völlig unsinnig, obwohl sie meiner Meinung nach emotional ansprechend ist. Ich bin mir nicht sicher, warum es so viele Stimmen hat. Haskell (oder noch wichtiger Idris) optimiert weitaus mehr auf Korrektheit als Java und hat Tupel mit einer leichten Syntax. Darüber hinaus ist das Definieren eines Datentyps ein Einzeiler, und Sie erhalten alles, was die Java-Version erhalten würde. Diese Antwort ist eine Entschuldigung für schlechtes Sprachdesign und unnötige Ausführlichkeit, aber es klingt gut, wenn Sie Java mögen und mit anderen Sprachen nicht vertraut sind.
Alexis King

50

Java und Python sind die beiden Sprachen, die ich am häufigsten verwende, aber ich komme aus der anderen Richtung. Das heißt, ich war tief in der Java-Welt, bevor ich anfing, Python zu verwenden, damit ich möglicherweise helfen kann. Ich denke, die Antwort auf die größere Frage "Warum sind die Dinge so schwer" besteht aus zwei Dingen:

  • Die Entwicklungskosten zwischen den beiden sind wie die Luft in einem langen Ballontierballon. Sie können einen Teil des Ballons zusammendrücken und ein anderer Teil schwillt an. Python neigt dazu, den frühen Teil zu quetschen. Java drückt den späteren Teil.
  • In Java fehlen noch einige Funktionen, die einen Teil dieses Gewichts entfernen würden. Java 8 hat eine große Beeinträchtigung davon verursacht, aber die Kultur hat die Änderungen nicht vollständig verdaut. Java könnte ein paar weitere Dinge gebrauchen, wie z yield.

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

In dem speziellen Fall, den Sie erwähnen, gibt es keine Tupel. Die einfache Lösung besteht darin, eine Klasse mit öffentlichen Werten zu erstellen. Als Java herauskam, taten die Leute dies ziemlich regelmäßig. Das erste Problem dabei ist, dass es Kopfschmerzen bei der Wartung gibt. Wenn Sie Logik oder Threadsicherheit hinzufügen oder Polymorphismus verwenden möchten, müssen Sie mindestens jede Klasse berühren, die mit diesem 'Tupel-ähnlichen' Objekt interagiert. In Python gibt es dafür Lösungen wie __getattr__etc., es ist also nicht so schlimm.

Es gibt jedoch einige schlechte Angewohnheiten (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 (abgesehen davon hasse ich die get / set-Konvention, aber es ist, was es ist.) Ich denke, eine nackte Klasse (veränderlich oder nicht) kann in einem privaten oder paket-privaten Kontext in Java nützlich sein . Das heißt, indem Sie die Referenzen im Projekt auf die Klasse beschränken, können Sie 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 generiert die Getter (und Setter, wenn Sie sie brauchen) für Sie, 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 anscheinend die meisten Ihrer Bedürfnisse hier erfüllt . Abgesehen davon ist der Ansatz, den Sie verwenden, für Java nicht geeignet, da er sich auf seine Schwächen und nicht auf seine Stärken auswirkt. 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;
  }
}

Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
maple_shaft

2
Darf ich vorschlagen, dass Sie sich Scala auch im Hinblick auf "modernere" Java-Funktionen ansehen ...
MikeW

1
@MikeW Ich habe mit Scala bereits in den frühen Releases angefangen und war eine Weile in den Scala-Foren aktiv. Ich denke, es ist eine großartige Leistung im Bereich Sprachdesign, aber ich bin zu dem Schluss gekommen, dass das nicht wirklich das ist, wonach ich gesucht habe und dass ich ein eckiger Partner in dieser Community bin. Ich sollte es mir noch einmal ansehen, da es sich seitdem wahrscheinlich erheblich verändert hat.
JimmyJames

Diese Antwort geht nicht auf den vom OP aufgeworfenen Aspekt des ungefähren Werts ein.
ErikE

@ErikE Die Wörter "ungefährer Wert" erscheinen nicht im Fragentext. Wenn Sie mich auf den spezifischen Teil der Frage verweisen können, den ich nicht angesprochen habe, kann ich dies versuchen.
JimmyJames

41

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

Eine Ihrer Beschwerden ist die Notwendigkeit, eine Klasse zu erstellen, um eine Reihe von Werten als Antwort zurückzugeben. Dies ist eine berechtigte Sorge, die 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 Besessenheits-Anti-Muster festhalten, dem Sie sich verpflichtet haben. Und Java kann nicht so einfach mit mehreren Primitiven arbeiten wie Python, bei dem Sie mehrere Werte nativ zurückgeben und sie einfach mehreren Variablen zuweisen können.

Aber wenn Sie überlegen, was ein ApproximateDurationTyp 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 Konzept, das von dieser Klasse dargestellt wird, ist tatsächlich eines Ihrer Kerngeschäftskonzepte für Domänen. Sie müssen in der Lage sein, die 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 ungefähre Zeitdauern zusammenfasst (oder Zeitdauern mit einer gewissen Fehlerquote, wie auch immer Sie dies darstellen), vollständig prozedural oder gibt es irgendwelche Einwände dagegen? Ich würde vorschlagen, dass ein gutes Design zum Zusammenfassen der ungefähren Dauer dies außerhalb jedes konsumierenden Codes innerhalb einer Klasse vorschreibt, die selbst getestet werden kann. Ich denke, dass die Verwendung dieser Art von Domänenobjekten eine positive Auswirkung auf Ihren Code hat, die Sie von zeilenweisen Verfahrensschritten ablenkt, um eine einzelne übergeordnete Aufgabe (wenn auch mit vielen Verantwortlichkeiten) in Richtung von Klassen mit nur einer Verantwortlichkeit zu erledigen das sind frei von Konflikten unterschiedlicher Bedenken.

Nehmen wir zum Beispiel an, Sie erfahren mehr darüber, welche Genauigkeit oder Skalierung tatsächlich erforderlich ist, damit Ihre Dauersummierung und Ihr Vergleich korrekt funktionieren, und Sie stellen fest, dass Sie ein Zwischenflag benötigen, um "Fehler von ungefähr 32 Millisekunden" (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 jeden Ort im Code finden, an dem Sie ihn haben, is_in_seconds,is_under_1msund ihn in ändernis_in_seconds,is_about_32_ms,is_under_1ms. Alles müsste sich überall ändern! Wenn Sie eine Klasse erstellen, deren Aufgabe es ist, die Fehlerquote so zu erfassen, dass sie an anderer Stelle konsumiert werden kann, müssen Ihre Verbraucher nicht mehr genau wissen, welche Fehlerquoten von Bedeutung sind oder wie sie kombiniert werden. Sie können lediglich die relevante Fehlerquote angeben in dem Augenblick. (Das heißt, kein konsumierender Code, dessen Fehlerspanne korrekt ist, muss sich ändern, wenn Sie der Klasse eine neue Fehlerspanne hinzufügen, da alle alten Fehlerspannen noch gültig sind.)

Schlusserklärung

Die Beschwerde über Javas Schwere scheint dann zu verschwinden, wenn Sie näher an die Prinzipien von SOLID und GRASP heranrücken und sich weiterentwickeln.

Nachtrag

Ich werde völlig unentgeltlich und unfair hinzufügen, dass die automatischen Eigenschaften von C # und die Fähigkeit, reine Get-Eigenschaften in Konstruktoren zuzuweisen, dazu beitragen, den etwas chaotischen Code, den "der Java-Weg" erfordert, noch weiter zu bereinigen (mit expliziten privaten Backing-Feldern 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 bestimmte Art von Wertklasse von entscheidender Bedeutung zu sein.

In dieser Hinsicht ist diese Klasse auch ein anständiger Kandidat für structeinen Wertetyp. Einige Tests werden zeigen, ob das Wechseln zu einer Struktur einen Leistungsvorteil zur Laufzeit hat (es könnte sein).


9
Ich denke, das ist meine Lieblingsantwort. Ich finde, wenn ich eine beschissene Klasse von Wegwerfbesitzern wie diese zu etwas Erwachsenem befördere, wird sie zum Zuhause für alle damit verbundenen Verantwortlichkeiten und für Zing! Alles wird sauberer! Und in der Regel lerne ich gleichzeitig etwas Interessantes über den Problemraum. Natürlich kann man ein Über-Engineering vermeiden ... aber Gosling war nicht dumm, als er die Tupelsyntax wegließ, genau wie bei GOTOs. Ihre Abschlusserklärung ist ausgezeichnet. Vielen Dank!
SusanW

2
Wenn Ihnen diese Antwort gefällt, informieren Sie sich über Domain-Driven Design. Zu diesem Thema der Darstellung von Domänenkonzepten im Code gibt es viel zu sagen.
Neontapir

@neontapir Ja, danke! Ich wusste, dass es einen Namen dafür gibt! :-) Obwohl ich sagen muss, dass ich es bevorzuge, wenn ein Domain-Konzept organisch aus einem inspirierten Teil des Refactorings (wie diesem!) Hervorgeht. .
SusanW

@SusanW Ich stimme zu. Refactoring zur Beseitigung primitiver Obsession kann eine hervorragende Möglichkeit sein, Domänenkonzepte aufzudecken!
Neontapir

Ich habe eine Java-Version des besprochenen Beispiels hinzugefügt. Ich bin nicht mit all dem magischen syntaktischen Zucker in C # beschäftigt. Wenn also etwas fehlt, lass es mich wissen.
JimmyJames

24

Sowohl Python als auch Java sind gemäß der Philosophie ihrer Designer auf Wartungsfreundlichkeit optimiert, sie haben jedoch sehr unterschiedliche Vorstellungen, wie dies erreicht werden kann.

Python ist eine Multiparadigmasprache, die für Klarheit und Einfachheit des Codes optimiert ist (leicht zu lesen und zu schreiben).

Java ist (traditionell) eine klassenbasierte OO-Sprache mit einem einzigen Paradigma, die sich durch Explizite und Konsistenz auszeichnet - selbst auf Kosten von ausführlicherem Code.

Ein Python-Tupel ist eine Datenstruktur mit einer festen Anzahl von Feldern. Dieselbe Funktionalität kann durch eine reguläre Klasse mit explizit deklarierten Feldern erreicht werden. In Python ist es selbstverständlich, 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. Es ist nicht erforderlich, eine andere Art von Datenstruktur einzuführen, nur um einige Codezeilen zu speichern und einige Deklarationen zu vermeiden.

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


+1, aber wie passt das zu statisch typisierten Sprachen mit Klassen wie Scala, die dennoch Tupel einführen mussten? Ich denke, Tupel sind in einigen Fällen nur die bessere Lösung, auch für Sprachen mit Klassen.
Andres F.

@AndresF .: Was meinst du mit mesh? Scala ist eine Sprache, die sich von Java unterscheidet und unterschiedliche Gestaltungsprinzipien und Redewendungen hat.
JacquesB

Ich meinte es als Antwort auf Ihren fünften Absatz, der mit "aber dies entspricht nicht wirklich der Java-Kultur [...]" beginnt. In der Tat stimme ich zu, dass Ausführlichkeit ein Teil der Kultur von Java ist, aber der Mangel an Tupeln kann nicht daran liegen, dass sie "bereits explizit deklarierte Klassen verwenden" - schließlich hat Scala auch explizite Klassen (und führt die prägnanteren Fallklassen ein ), aber es erlaubt auch Tupel! Letztendlich denke ich, dass Java wahrscheinlich keine Tupel eingeführt hat, nicht nur, weil Klassen dasselbe erreichen können (mit mehr Durcheinander), sondern auch, weil die Syntax C-Programmierern vertraut sein muss und C keine Tupel hatte.
Andres F.

@AndresF .: Scala hat keine Abneigung gegen mehrere Arten, Dinge zu tun. Es kombiniert Funktionen sowohl aus dem funktionalen Paradigma als auch aus dem klassischen OO. Auf diese Weise ist es der Python-Philosophie näher.
JacquesB

@AndresF .: Ja, Java begann mit einer Syntax, die der von C / C ++ sehr nahe kommt, hat sich aber stark vereinfacht. C # war ursprünglich eine fast exakte Kopie von Java, hat aber im Laufe der Jahre viele Funktionen hinzugefügt, darunter Tupel. Es ist also wirklich ein Unterschied in der Philosophie des Sprachdesigns. (Spätere Versionen von Java scheinen jedoch weniger dogmatisch zu sein. Funktionen höherer Ordnung wurden anfangs abgelehnt, da Sie dieselben Klassen verwenden konnten, aber jetzt wurden sie eingeführt. Vielleicht sehen wir eine Änderung in der Philosophie.)
JacquesB

16

Suche nicht nach Übungen; Es ist in der Regel eine schlechte Idee, wie in Best Practices BAD gesagt , Muster gut? . Ich weiß, dass Sie nicht nach Best Practices fragen, aber ich denke immer noch, dass Sie einige relevante Elemente darin finden werden.

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

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

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 öffentlich ist, sodass keine Getter erforderlich sind. Beachten Sie jedoch, dass bei Verwendung einiger Serialisierungstools oder Persistenz-Layer wie ORM üblicherweise Getter / Setter verwendet werden (und möglicherweise ein Parameter zur Verwendung von Feldern anstelle von Getter / Setter akzeptiert wird). 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.

Zum Schluss: 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.


9
Es ist immer noch wichtig, gemeinsame Redewendungen zu verstehen, die Sie in verschiedenen Sprachen kennenlernen, da sie de facto zu einem zugänglicheren Standard werden. Diese Redewendungen fallen aus welchen Gründen auch immer unter die Überschrift "Best Practices".
Berin Loritsch

3
Hey, eine vernünftige Lösung für das Problem. Es erscheint vernünftig, nur eine Klasse (/ struct) mit wenigen öffentlichen Mitgliedern zu schreiben, anstatt eine ausgereifte, überentwickelte OOP-Lösung mit [gs] etters.
Tomsmeding

3
Dies führt zu Blähungen, da im Gegensatz zu Python die Suche oder eine kürzere und elegantere Lösung nicht gefördert wird. Der Vorteil ist jedoch, dass das Aufblähen für jeden Java-Entwickler in etwa gleich ist. Daher gibt es einen gewissen Grad an Wartbarkeit, da Entwickler austauschbarer sind und wenn zwei Projekte zusammengeführt werden oder zwei Teams unabsichtlich auseinander gehen, gibt es weniger Stilunterschiede zu kämpfen.
Mikhail Ramendik

2
@MikhailRamendik Es ist ein Kompromiss zwischen YAGNI und OOP. Orthodoxe OOP sagt, dass jedes einzelne Objekt austauschbar sein sollte; Das einzige, was zählt, ist die öffentliche Schnittstelle des Objekts. Auf diese Weise können Sie Code schreiben, der sich weniger auf konkrete Objekte als vielmehr auf Schnittstellen konzentriert. und da Felder keine Teile einer Schnittstelle sein können, legen Sie sie niemals offen. Dies kann die Pflege von Code auf lange Sicht erheblich vereinfachen. Außerdem können Sie damit ungültige Zustände verhindern. In Ihrem ursprünglichen Beispiel ist es möglich, ein Objekt zu haben, das sowohl "<ms" als auch "s" ist, die sich gegenseitig ausschließen. Das ist schlecht.
Luaan

2
@PeterMortensen Es handelt sich um eine Java-Bibliothek, die eine Reihe von Dingen ausführt, die in keinem Zusammenhang stehen. Es ist aber sehr gut. Hier sind die Features
Michael

11

Über Java-Redewendungen im Allgemeinen:

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

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


In Bezug auf Ihr spezielles Beispiel lautet die Argumentationslinie für eine separate Klasse wie folgt: Wenn diese drei Dinge stark genug miteinander verwandt sind, dass sie als ein Wert zurückgegeben werden, lohnt es sich, dieses "Ding" zu nennen. Und die Einführung eines Namens für eine Gruppe von Dingen, die auf eine gemeinsame Weise strukturiert sind, bedeutet, eine Klasse zu definieren.

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

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

5
Das ist auch Googles AutoValue-Projekt: github.com/google/auto/blob/master/value/userguide/index.md
Ivan

Dies ist auch der Grund, warum Java-Programme relativ einfach gewartet werden können!
Thorbjørn Ravn Andersen

7

Es gibt eine Menge Dinge, die über die Java-Kultur gesagt werden könnten, aber ich denke, dass es in dem Fall, in dem Sie gerade konfrontiert sind, ein paar wichtige Aspekte gibt:

  1. Bibliothekscode wird einmal geschrieben, aber viel häufiger verwendet. Während es nett ist, den Aufwand für das Schreiben der Bibliothek zu minimieren, ist es wahrscheinlich auf lange Sicht sinnvoller, so zu schreiben, dass der Aufwand für die Verwendung der Bibliothek minimiert wird.
  2. Das bedeutet, dass selbstdokumentierende Typen großartig sind: Methodennamen helfen dabei, klar zu machen, 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 witzeln gern über Haskell, dass es wahrscheinlich richtig ist, wenn Sie das Typensystem erst einmal dazu bringen, Ihren Code zu akzeptieren), aber es macht es sehr einfach, bestimmte Arten von falschen Dingen unmöglich.
  4. Beim Schreiben von Bibliothekscode geht es um die Angabe von Verträgen. Durch das Definieren von Schnittstellen für Ihre Argumente und Ergebnistypen werden die Grenzen Ihrer Verträge klarer definiert. Wenn etwas ein Tupel akzeptiert oder erzeugt, gibt es keine Aussage darüber, ob es sich um die Art von Tupel handelt, die Sie tatsächlich erhalten oder erzeugen sollten, und es gibt kaum Einschränkungen für einen solchen generischen Typ (hat er überhaupt die richtige Anzahl von Elementen?) sie von der Art, die Sie erwartet hatten?).

"Struct" -Klassen mit Feldern

Wie andere Antworten bereits erwähnt haben, können Sie eine Klasse nur mit öffentlichen Feldern verwenden. Wenn Sie diese final machen, erhalten Sie eine unveränderliche Klasse und Sie initialisieren sie mit dem Konstruktor:

   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 alles, was jemals ein Analyseergebnis erzeugen oder verarbeiten muss, muss diese Klasse verwenden. Für einige Anwendungen ist das in Ordnung. Bei anderen kann dies Schmerzen verursachen. Bei viel Java-Code geht es um die Definition von Verträgen, und das führt Sie in der Regel zu Schnittstellen.

Ein weiterer Fallstrick ist , dass mit einer Klasse basierten Ansatz, Sie Felder sind Belichtungs- und alle diese Felder müssen Werte haben. ZB 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 Interfaces erlaubt 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, folgendermaßen 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 bin ich mir absolut einig, aber es gibt auch ein paar Vorteile, und ich denke, dass diese anfangen, einige Ihrer Hauptfragen zu beantworten.

Mit einer Struktur wie diesem Analyseergebnis ist der Vertrag Ihres Parsers sehr klar definiert. In Python unterscheidet sich ein Tupel nicht wirklich von einem anderen Tupel. In Java ist die statische Typisierung verfügbar, sodass bestimmte Fehlerklassen bereits ausgeschlossen sind. Wenn Sie beispielsweise ein Tupel in Python zurückgeben und das Tupel zurückgeben möchten (millis, isSeconds, isLessThanOneMilli), 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);

Das ist ein Vorteil von statisch typisierten Sprachen im Allgemeinen.

Mit diesem Ansatz können Sie auch einschränken, welche Werte Sie erhalten können. Wenn Sie zum Beispiel getMillis () aufrufen, können Sie überprüfen, ob isLessThanOneMilli () wahr ist, und wenn dies der Fall ist, lösen Sie (zum Beispiel) eine IllegalStateException aus, da in diesem Fall kein sinnvoller Wert für millis vorliegt.

Es schwierig machen, das Falsche zu tun

Im obigen Schnittstellenbeispiel besteht jedoch weiterhin das Problem, dass Sie die Argumente isSeconds und isLessThanOneMilli versehentlich vertauschen könnten, da sie denselben Typ haben.

In der Praxis sollten Sie TimeUnit und duration wirklich verwenden, damit Sie das folgende Ergebnis erzielen:

   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 ist immer ein zu viel mehr Code, aber Sie müssen es nur einmal schreiben, und (vorausgesetzt , Sie richtig Dinge dokumentiert), die Menschen , die am Ende mit Ihrem Code müssen nicht erraten , was das Ergebnis bedeutet, und kann nicht versehentlich Dinge tun, wie result[0]wenn sie bedeuten result[1]. Sie können Instanzen immer noch ziemlich prägnant 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 die 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 darauf nicht verhindern.

In einer anderen Antwort wurde erwähnt, dass aufgrund des Enterprise-Charakters von Java häufig andere Bibliotheken erstellt werden, die bereits vorhanden sind, oder Bibliotheken für andere Benutzer geschrieben werden. Ihre öffentliche API sollte nicht viel Zeit benötigen, um die Dokumentation zu konsultieren und die Ergebnistypen zu entschlüsseln, wenn dies vermieden werden kann.

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

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


Ich habe am Ende meine eigene Enumeration anstelle von TimeUnit verwendet, weil ich gerade UNDER1MS zusammen mit den tatsächlichen Zeiteinheiten hinzugefügt habe. Jetzt habe ich also nur zwei Felder, nicht drei. (Theoretisch könnte ich TimeUnit.NANOSECONDS missbrauchen, aber das wäre sehr verwirrend.)
Mikhail Ramendik

Ich habe keinen Getter erstellt, aber jetzt sehe ich, wie ein Getter es mir ermöglichen würde, eine Ausnahme auszulösen, wenn originalTimeUnit=DurationTimeUnit.UNDER1MSder Aufrufer mit versucht hätte, den Millisekundenwert zu lesen.
Mikhail Ramendik

Ich weiß, dass Sie es erwähnt haben, aber denken Sie daran, dass es in Ihrem Beispiel mit Python-Tupeln wirklich um dynamische versus statisch typisierte Tupel geht. Es sagt nicht viel über Tupel aus. Sie können statisch typisierte Tupel haben, die nicht kompiliert werden können, wie Sie sicher wissen :)
Andres F.

1
@Veedrac Ich bin mir nicht sicher, was du meinst. Mein Punkt war, dass es in Ordnung ist, Bibliothekscode so ausführlicher zu schreiben (obwohl es länger dauert), da mehr Zeit mit dem Code verbracht wird als mit dem Schreiben .
Joshua Taylor

1
@Veedrac Ja, das stimmt. Aber ich würde behaupten , dass es ist eine Unterscheidung zwischen Code, der sinnvoll wiederverwendet werden, und Code, der nicht. In einem Java-Paket sind beispielsweise einige Klassen öffentlich und werden von anderen Standorten aus verwendet. Diese APIs sollten gut durchdacht sein. Intern gibt es Klassen, die möglicherweise nur miteinander sprechen müssen. Diese internen Kopplungen sind die Stellen, an denen schnelle und schmutzige Strukturen (z. B. ein Objekt [], von dem drei Elemente erwartet werden) in Ordnung sind. Für die öffentliche API sollte normalerweise etwas aussagekräftigeres und expliziteres bevorzugt werden.
Joshua Taylor

7

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

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

Das ist auch gar nicht so 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 nicht das kulturelle Problem.

Und schließlich überdenken Sie den Versuch, etwas zu beschleunigen, das überhaupt nicht wichtig ist. Die für das Schreiben von DTO-Klassen aufgewendete Zeit ist im Vergleich zu der Zeit, die für die Wartung und das Debuggen des Systems aufgewendet wurde, unbedeutend. Deshalb optimiert niemand für weniger Ausführlichkeit.


2
"Nobody" ist faktisch falsch - es gibt eine Menge Tools, die für weniger Ausführlichkeit optimiert sind, reguläre Ausdrücke sind ein extremes Beispiel. Aber Sie hätten vielleicht "niemand in der Mainstream-Java-Welt" gemeint, und in dieser Lektüre macht die Antwort sehr viel Sinn, danke. Meine eigene Sorge um die Ausführlichkeit ist nicht die Zeit, die ich für das Schreiben von Code aufgewendet habe, sondern die Zeit, die ich für das Lesen des Codes aufgewendet habe. Die IDE spart keine Lesezeit. aber es scheint die Idee zu sein, dass man zu 99% der Zeiten nur die Schnittstellendefinition liest, damit DAS prägnant sein sollte?
Mikhail Ramendik

5

Dies sind drei verschiedene Faktoren, die zu Ihrer Beobachtung beitragen.

Tupel gegen benannte Felder

Vielleicht das Trivialste - in den anderen Sprachen haben Sie ein Tupel verwendet. Zu diskutieren, ob Tupel eine gute Idee sind, ist nicht wirklich der springende Punkt - aber in Java haben Sie eine schwerere Struktur verwendet, sodass 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 etwas wie Scalas Fallklassen, die alle Vorteile des von Ihnen beschriebenen Aufbaus bieten, aber viel prägnanter sind:

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

Wir könnten das haben - aber es gibt einen Preis: Die Syntax wird komplizierter. Natürlich könnte es sich in einigen Fällen oder sogar in den meisten Fällen oder sogar in den meisten Fällen für die nächsten 5 Jahre lohnen - aber es muss beurteilt werden. Übrigens ist dies eines der schönen Dinge mit Sprachen, die Sie selbst modifizieren können (dh lisp) - und beachten Sie, wie dies aufgrund der einfachen 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, aber nicht für Scala verfügbar sind.

Sprachphilosophie

Das Wichtigste ist jedoch, dass eine Sprache eine bestimmte Denkweise ermöglicht. Manchmal mag es sich bedrückend anfühlen (ich habe mir oft gewünscht, dass eine bestimmte Funktion unterstützt wird), aber das Entfernen von Funktionen ist genauso wichtig wie das Vorhandensein. Könntest du alles unterstützen? Klar, aber dann können Sie genauso gut 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 Untermenge annehmen.

Es ist natürlich möglich, Code zu schreiben, der der Philosophie der Sprache widerspricht, und wie Sie gesehen haben, sind die Ergebnisse oft hässlich. Eine Klasse mit nur ein paar Feldern in Java zu haben, ist vergleichbar mit der Verwendung einer Var in Scala, dem Degenerieren von Prolog-Prädikaten zu Funktionen, dem Ausführen einer unsicherenPerformIO in Haskell usw. Java-Klassen sind nicht dazu gedacht, leicht zu sein - sie sind nicht dazu da, Daten weiterzugeben . Wenn etwas schwierig erscheint, ist es oft fruchtbar, zurückzutreten und zu prüfen, ob es einen anderen Weg gibt. In deinem Beispiel:

Warum ist die Laufzeit von den Einheiten getrennt? Es gibt viele Zeitbibliotheken, mit denen Sie eine Dauer deklarieren können - so etwas wie Dauer (5, Sekunden) (die Syntax variiert), mit der Sie dann viel robuster tun können, was Sie wollen. Vielleicht möchten Sie es konvertieren - warum überprüfen Sie, ob das Ergebnis [1] (oder [2]?) Eine Stunde ist und mit 3600 multipliziert wird? Und für das dritte Argument - wozu dient es? 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. Dh du solltest 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, um die Logik zu verkapseln.

Natürlich könnte es einen Fall geben, in dem dies nicht funktioniert - ich sage nicht, dass dies der magische Algorithmus zum Konvertieren von Tupelergebnissen 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!

Ich bin jedoch der Ansicht, dass Klassen in Java "schwere Strukturen" sind und nicht als Datencontainer, sondern als in sich geschlossene Zellen der Logik verwendet werden sollen.


Was ich will , zu tun mit der Dauer geht es eigentlich um eine Summe von ihnen zu machen und dann zu einem anderen zu vergleichen. Keine Ausgabe beteiligt. Der Vergleich muss die Rundung berücksichtigen, daher muss ich die ursprünglichen Zeiteinheiten zum Zeitpunkt des Vergleichs kennen.
Mikhail Ramendik

Es wäre wahrscheinlich möglich, eine Methode zum Hinzufügen und Vergleichen von Instanzen meiner Klasse zu erstellen, aber dies könnte sehr schwer werden. Zumal ich auch durch Floats dividieren und multiplizieren muss (in dieser Benutzeroberfläche müssen Prozentsätze überprüft werden). Deshalb habe ich vorerst eine unveränderliche Klasse mit endgültigen Feldern gemacht, was wie ein netter Kompromiss aussieht.
Mikhail Ramendik

Der wichtigere Teil dieser speziellen Frage war die allgemeinere Seite der Dinge, und von allen Antworten scheint ein Bild zusammen zu kommen.
Mikhail Ramendik

@MikhailRamendik vielleicht wäre eine Art Akku eine Option - aber du kennst das Problem besser. In der Tat geht es nicht darum, dieses spezielle Problem zu lösen. Tut mir leid, wenn ich etwas abgelenkt wurde. Mein Hauptpunkt ist, dass die Sprache von der Verwendung "einfacher" Daten abhält. Manchmal hilft Ihnen dies, Ihre Herangehensweise zu überdenken - manchmal ist ein Int nur ein Int
Thanos Tintinidis

@MikhailRamendik Das Schöne am Boilerplatey-Verfahren ist, dass Sie, sobald Sie eine Klasse haben, ihr Verhalten hinzufügen können. Und / oder machen Sie es unveränderlich, wie Sie es getan haben (dies ist meistens eine gute Idee; Sie können immer ein neues Objekt als Summe zurückgeben). Am Ende kann Ihre "überflüssige" Klasse alles, was Sie brauchen, verkapseln, und dann werden die Kosten für die Getter vernachlässigbar. Darüber hinaus können Sie sie benötigen oder nicht. Erwägen Sie die Verwendung von Lombok oder Autovalue .
Maaartinus

5

Nach meinem Verständnis sind die Hauptgründe

  1. Schnittstellen sind die grundlegende Java-Methode zum Abstrahieren von Klassen.
  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, nur Methoden. Wenn Sie auf ein Feld zugreifen möchten, müssen Sie eine Methode durchlaufen - also Getter und Setter.
  4. Wenn eine Methode eine Schnittstelle zurückgibt, müssen Sie eine implementierende Klasse haben, um tatsächlich zurückzugeben.

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


Hinzu kommt: Wenn Sie dies nur in einem kleinen Kontext benötigen (z. B. einer privaten Methode in einer Klasse), ist es in Ordnung, eine leere Aufzeichnung zu verwenden - im Idealfall unveränderlich. Aber es sollte niemals in den öffentlichen Vertrag der Klasse (oder noch schlimmer in die Bibliothek!) Eingehen. Sie können sich gegen Datensätze entscheiden, um sicherzugehen, dass dies bei zukünftigen Codeänderungen niemals der Fall ist. es ist oft die einfachere Lösung.
Luaan

Und ich stimme der "Tuples funktionieren in Java nicht gut" -Ansicht zu. Wenn Sie Generika verwenden, wird dies sehr schnell böse enden.
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen Sie funktionieren ziemlich gut in Scala, einer statisch typisierten Sprache mit Generika und Tupeln. Vielleicht ist das Javas Schuld?
Andres F.

@AndresF. Bitte beachten Sie den Abschnitt "Wenn Sie Generika verwenden". Die Art und Weise, wie Java dies tut, eignet sich nicht für komplexe Konstruktionen im Gegensatz zu zB Haskell. Ich kenne Scala nicht gut genug, um einen Vergleich anzustellen, aber ist es richtig, dass Scala mehrere Rückgabewerte zulässt? Das könnte sehr viel helfen.
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen Scala-Funktionen haben einen einzelnen Rückgabewert, der vom Typ Tupel sein kann (z. B. def f(...): (Int, Int)eine Funktion, fdie einen Wert zurückgibt, der zufällig ein Tupel von ganzen Zahlen ist). Ich bin mir nicht sicher, ob Generika in Java das Problem sind. Beachten Sie, dass Haskell beispielsweise auch die Löschung von Texten ausführt. Ich glaube nicht, dass es einen technischen Grund gibt, warum Java keine Tupel hat.
Andres F.

3

Ich stimme JacquesB zu

Java ist (traditionell) eine klassenbasierte OO-Sprache mit einem einzigen Paradigma, die sich durch Explizite und Konsistenz auszeichnet

Aber Explizite und Beständigkeit sind nicht die Endziele, für die optimiert werden muss. 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 Java-Methoden anwenden? Ich gehe davon aus, dass es sich um eine Sprache handelt, die behauptet, vorhersehbare, konsistente und einheitliche Möglichkeiten zur Lösung von Softwareproblemen zu bieten.

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

Oder, wie es ein Weiser vor langer Zeit ausdrückte ,

Der beste Weg, eine Sprache zu beurteilen, besteht darin, sich den Code anzusehen, den seine Befürworter geschrieben haben. "Radix enim omnium malorum est cupiditas" - und Java ist eindeutig ein Beispiel für eine geldorientierte Programmierung (MOP). Wie der Hauptvertreter von Java bei SGI mir sagte: "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

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

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 sich manchmal negativ auf das andere auswirken (z. B. kann es auch schwieriger sein, 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 des betreffenden Teams und der Kultur (der Organisation oder der Sprachgemeinschaft) ab.

Wenn zum Beispiel ein Fehler oder ein Ausfall von ein paar Stunden in Ihrem Programm zum Verlust von Menschenleben (medizinische Systeme, Luftfahrt) oder sogar nur Geld (wie Millionen von Dollar in den Anzeigensystemen von Google) führen könnte, würden Sie unterschiedliche 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 eine große Sorge darin, dass jemand die API eines anderen falsch verwendet. Eine Funktion, die mit Argumenten in der falschen Reihenfolge aufgerufen wird, oder die aufgerufen wird, ohne die erwarteten Voraussetzungen / Einschränkungen zu erfüllen, kann katastrophal sein.
  • In diesem speziellen Fall kann Ihr Team beispielsweise eine bestimmte API oder Bibliothek verwalten und diese ändern oder überarbeiten. 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 ist, tatsächliche Garantien dafür zu haben, dass niemand es auf ungewöhnliche Weise verwenden kann.)
  • Wenn die Entwicklung auf mehrere Personen oder Teams aufgeteilt ist, ist es möglicherweise eine gute Idee, wenn eine Person oder ein Team die Benutzeroberfläche „spezifiziert“ und andere sie tatsächlich implementieren. Damit dies funktioniert, müssen Sie in der Lage sein, ein gewisses Maß an Vertrauen zu gewinnen, wenn die Implementierung abgeschlossen ist, dass die Implementierung tatsächlich der Spezifikation entspricht.

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

Ein Beispiel: Googles internes Python-System macht Dinge so „schwer“, dass 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 deklarieren sichtbar für Ihren Code usw.


Hinweis : Alles oben Genannte gilt nur, wenn die Dinge dazu neigen, "schwer" zu werden. Ich behaupte absolut nicht , dass Java oder Python (entweder die Sprachen selbst oder ihre Kulturen) für einen bestimmten Fall optimale Kompromisse eingehen; darüber musst du nachdenken. Zwei verwandte Links zu solchen Kompromissen:


1
Was ist mit dem Lesen von Code? Da gibt es noch einen Kompromiss. Code, der durch bizarre Beschwörungsformeln und üppige Rituale beschwert wird, ist schwerer zu lesen. Mit Boilerplate und unnötigem Text (und Klassenhierarchien!) in Ihrer IDE wird es schwieriger zu sehen, was der Code tatsächlich tut. Ich kann das Argument sehen, dass Code nicht unbedingt einfach zu schreiben sein sollte (nicht sicher, ob ich damit einverstanden bin, aber es hat einige Vorzüge), aber es sollte auf jeden Fall einfach zu lesen sein . Offensichtlich komplexer Code kann und wurde mit Java gebaut - es wäre töricht, anders zu behaupten - aber war es dank seiner Ausführlichkeit oder in trotz davon?
Andres F.

@AndresF. Ich habe die Antwort bearbeitet, um zu verdeutlichen, dass es nicht um Java geht (von dem ich kein großer Fan bin). Aber ja, Kompromisse beim Lesen von Code: An einem Ende möchten Sie einfach lesen können, was der Code tatsächlich tut, wie Sie sagten. Am anderen Ende möchten Sie in der Lage sein, Antworten auf folgende Fragen leicht zu sehen: In welcher Beziehung steht dieser Code zu anderem Code? Was ist das Schlimmste, was dieser Code anrichten könnte: Welche Auswirkungen hat er auf andere Zustände, welche Nebenwirkungen hat er? Von welchen externen Faktoren könnte dieser Code möglicherweise abhängen? (Natürlich möchten wir im Idealfall schnelle Antworten auf beide Fragen.)
ShreevatsaR

2

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 man wirklich darüber nachdenkt. Enterprise-Lö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 als lesbar und wartbar angesehen wird.

  • In Python ist das Tupel eine Sprache - Funktion.
  • Sowohl in Java als auch in C # ist (oder wäre) das Tupel eine Bibliotheksfunktion .

Ich erwähne nur C #, weil die Standardbibliothek eine Reihe von Tuple <A, B, C, .. n> -Klassen enthält und dies 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 besser lesbar und kann besser gewartet werden, wenn Sie Klassen ausgewählt haben, um das Problem zu beheben. In dem speziellen Beispiel in Ihrer verknüpften Stapelüberlauf-Frage können die anderen Werte leicht als berechnete Get-Werte für das Rückgabeobjekt ausgedrückt werden.

Eine interessante Lösung für die C # -Plattform, die einen guten Mittelweg bietet, ist die Idee anonymer Objekte (veröffentlicht in C # 3.0 ), die dieses Problem ganz gut lösen. Leider hat Java 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 viele weitere Sprachfunktionen geplant, die es nie geschafft haben, und die Abwärtskompatibilität ist eine der Haupteinschränkungen für die Entwicklung von Java im Laufe der Zeit.


4
In der neuesten Version von c # sind die neuen Tupel eher erstklassige Bürger als Bibliotheksklassen. Es dauerte zu viele Versionen, um dorthin zu gelangen.
Igor Soloydenko

Die letzten drei Absätze können zusammengefasst werden, da "Sprache X die Funktion hat, die Sie suchen, die Java nicht hat", und ich verstehe nicht, was sie zur Antwort hinzufügen. Warum C # in einem Python-zu-Java-Thema erwähnen? Nein, ich verstehe den Punkt einfach nicht. Ein bisschen komisch von einem 20k + rep Kerl.
Olivier Grégoire

1
Wie gesagt: "Ich erwähne nur C #, weil Tupel eine Bibliotheksfunktion sind", ähnlich wie dies in Java funktionieren würde, wenn Tupel in der Hauptbibliothek vorhanden wären. Es ist sehr unhandlich zu benutzen, und die bessere Antwort ist fast immer, eine eigene Klasse zu haben. Die anonymen Objekte jucken ähnlich wie Tupel. Ohne Sprachunterstützung für etwas Besseres müssen Sie im Wesentlichen mit dem arbeiten, was am besten zu warten ist.
Berin Loritsch

@IgorSoloydenko, vielleicht gibt es Hoffnung für Java 9? Dies ist nur eine dieser Eigenschaften, die der logische nächste Schritt zu Lambdas sind.
Berin Loritsch

1
@IgorSoloydenko Ich bin mir nicht sicher, ob das ganz richtig ist (Bürger erster Klasse) - es scheint sich um Syntaxerweiterungen zu handeln, mit denen Instanzen von Klassen aus der Bibliothek erstellt und entpackt werden können, statt erstklassige Unterstützung in der CLR wie class, struct, enum do zu haben.
Pete Kirkham

0

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 sie nicht sind. Wann würden Sie einen BMI mit dem Gewicht einer Person und der Größe einer anderen Person berechnen? Das 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.


4
Ok, aber nehmen wir an, Sie haben dann eine (hypothetische) Aufgabe, ein Diagramm anzuzeigen: Wie hängt der BMI für eine bestimmte feste Größe vom Gewicht ab? Dann können Sie dies nicht tun, ohne für jeden Datenpunkt eine Person zu erstellen. Im Extremfall können Sie keine Instanz der Personenklasse ohne gültiges Geburtsdatum und gültige Passnummer erstellen. Was jetzt? Ich habe übrigens nicht abgelehnt, es gibt triftige Gründe, Eigenschaften in einer Klasse zu bündeln.
Artem

1
@artem das ist der nächste schritt, bei dem schnittstellen ins spiel kommen. Eine personweightheight-Schnittstelle könnte also so etwas sein. Sie werden in das Beispiel verwickelt, das ich gegeben habe, um darauf hinzuweisen, dass das, was zusammen gehört, zusammen sein sollte.
Pieter B

0

Um ehrlich zu sein, besteht die Kultur darin, dass Java-Programmierer ursprünglich eher von Universitäten kamen, an denen objektorientierte Prinzipien und nachhaltige Software-Design-Prinzipien gelehrt wurden.

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

In der Java-Kultur ist Ihnen in der Regel bewusst, welche Bibliotheken verfügbar sind, und so können Sie weitaus mehr erreichen, als Sie selbst programmieren. Sie würden also Ihre Eigenheiten gegen die Designmuster und -stile austauschen, die in industriellen Umgebungen mit hartem Kern erprobt wurden.

Aber wie Sie sagen, ist dies nicht ohne Nachteile: Heute, da ich seit über 10 Jahren Java verwende, verwende ich für neue Projekte entweder Node / Javascript oder Go, da beide eine schnellere Entwicklung ermöglichen und dies bei Architekturen im Mikroservice-Stil der Fall sind oft genug. Gemessen an der Tatsache, dass Google zuerst stark Java verwendete, aber der Urheber von Go war, denke ich, dass sie dasselbe tun werden. Aber obwohl ich jetzt Go und Javascript benutze, verwende ich immer noch viele der Designfähigkeiten, die ich durch jahrelange Verwendung und Verständnis von Java erhalten habe.


1
Insbesondere das von Google verwendete Rekrutierungs-Tool foobar ermöglicht die Verwendung von zwei Sprachen für Lösungen: Java und Python. Ich finde es interessant, dass Go keine Option ist. Ich bin auf diese Präsentation auf Go gestoßen und sie enthält einige interessante Punkte zu Java und Python (sowie zu anderen Sprachen). Zum Beispiel ist ein Aufzählungspunkt aufgefallen: "Vielleicht haben nicht-erfahrene Programmierer dadurch die Leichtigkeit verwechselt." Verwenden Sie "mit Interpretation und dynamischer Eingabe". Lohnt sich zu lesen.
JimmyJames

@JimmyJames kam gerade zu der Folie mit der Aufschrift "In den Händen von Experten, sie sind großartig" - gute Zusammenfassung des Problems in der Frage
Tom
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.