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.
quelle
Antworten:
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.
quelle
for
Schleife zu schreiben , wenn einmap
besser geeignet (und lesbarer) wäre. Es gibt ein fröhliches Medium.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:
Verwenden Sie
Triplet<A, B, C>
als Rückgabetyp (was wirklich in sein solltejava.util
, und ich kann nicht wirklich erklären , warum es nicht ist. Vor allem , da JDK8 einführenFunction
,BiFunction
,Consumer
,BiConsumer
, etc ... Es scheint nur , dassPair
undTriplet
zumindest Sinn machen würde. Aber ich schweife ab )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.
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.Duration
Objekt zurückgegeben werden kann, was viel expliziter wäre als die beiden oben genannten Fälle.quelle
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:
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:
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:
quelle
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
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 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_1ms
und 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). :
Hier ist eine Java-Implementierung des oben genannten:
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
struct
einen Wertetyp. Einige Tests werden zeigen, ob das Wechseln zu einer Struktur einen Leistungsvorteil zur Laufzeit hat (es könnte sein).quelle
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.
quelle
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:
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.
quelle
Ü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:
quelle
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:
"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:
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:
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:
als du meintest:
Mit dieser Art von Java-Schnittstelle können Sie nicht kompilieren:
überhaupt. Du musst:
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:
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 bedeutenresult[1]
. Sie können Instanzen immer noch ziemlich prägnant erstellen , und es ist auch nicht allzu schwierig, Daten aus ihnen herauszuholen: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.
quelle
originalTimeUnit=DurationTimeUnit.UNDER1MS
der Aufrufer mit versucht hätte, den Millisekundenwert zu lesen.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.
quelle
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:
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:
}
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.
quelle
Nach meinem Verständnis sind die Hauptgründe
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.
quelle
def f(...): (Int, Int)
eine Funktion,f
die 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.Ich stimme JacquesB zu
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 ,
quelle
(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:
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:
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:
quelle
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.
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.
quelle
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:
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.
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.
quelle
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.
quelle