Schnittstellen mit statischen Feldern in Java zum Teilen von 'Konstanten'

116

Ich schaue mir einige Open-Source-Java-Projekte an, um in Java einzusteigen, und stelle fest, dass viele von ihnen eine Art 'Konstanten'-Schnittstelle haben.

Zum Beispiel processing.org verfügt über eine Schnittstelle namens PConstants.java , und die meisten anderen Hauptklassen implementieren diese Schnittstelle. Die Schnittstelle ist mit statischen Elementen durchsetzt. Gibt es einen Grund für diesen Ansatz oder wird dies als schlechte Praxis angesehen? Warum nicht Enums verwenden, wo es Sinn macht , oder eine statische Klasse?

Ich finde es seltsam, eine Schnittstelle zu verwenden, um eine Art pseudo-globaler Variablen zuzulassen.

public interface PConstants {

  // LOTS OF static fields...

  static public final int SHINE = 31;

  // emissive (by default kept black)
  static public final int ER = 32;
  static public final int EG = 33;
  static public final int EB = 34;

  // has this vertex been lit yet
  static public final int BEEN_LIT = 35;

  static public final int VERTEX_FIELD_COUNT = 36;


  // renderers known to processing.core

  static final String P2D    = "processing.core.PGraphics2D";
  static final String P3D    = "processing.core.PGraphics3D";
  static final String JAVA2D = "processing.core.PGraphicsJava2D";
  static final String OPENGL = "processing.opengl.PGraphicsOpenGL";
  static final String PDF    = "processing.pdf.PGraphicsPDF";
  static final String DXF    = "processing.dxf.RawDXF";


  // platform IDs for PApplet.platform

  static final int OTHER   = 0;
  static final int WINDOWS = 1;
  static final int MACOSX  = 2;
  static final int LINUX   = 3;

  static final String[] platformNames = {
    "other", "windows", "macosx", "linux"
  };

  // and on and on

}
Kitsune
quelle
15
Hinweis: static finalist nicht erforderlich, es ist für eine Schnittstelle redundant.
ThomasW
Beachten Sie auch , dass platformNamessein kann public, staticund final, aber es ist definitiv nicht eine Konstante. Das einzige konstante Array ist eins mit der Länge Null.
Vlasec
@ThomasW Ich weiß, dass dies ein paar Jahre alt ist, aber ich musste auf einen Fehler in Ihrem Kommentar hinweisen. static finalist nicht unbedingt redundant. Ein Klassen- oder Schnittstellenfeld mit nur dem finalSchlüsselwort würde separate Instanzen dieses Felds erstellen, wenn Sie Objekte der Klasse oder Schnittstelle erstellen. Durch static finaldie Verwendung würde jedes Objekt einen Speicherort für dieses Feld gemeinsam nutzen. Mit anderen Worten, wenn eine Klasse MyClass ein Feld final String str = "Hello";hätte, wären für N Instanzen von MyClass N Instanzen des Feldes str im Speicher. Das Hinzufügen des staticSchlüsselworts würde nur zu einer Instanz führen.
Sintrias

Antworten:

160

Es wird allgemein als schlechte Praxis angesehen. Das Problem ist, dass die Konstanten Teil der öffentlichen "Schnittstelle" (mangels eines besseren Wortes) der implementierenden Klasse sind. Dies bedeutet, dass die implementierende Klasse alle diese Werte für externe Klassen veröffentlicht, auch wenn sie nur intern benötigt werden. Die Konstanten vermehren sich im gesamten Code. Ein Beispiel ist die SwingConstants- Schnittstelle in Swing, die von Dutzenden von Klassen implementiert wird, die alle ihre Konstanten (auch diejenigen, die sie nicht verwenden) als ihre eigenen " reexportieren " .

Aber nimm nicht nur mein Wort dafür, Josh Bloch sagt auch, dass es schlecht ist:

Das konstante Schnittstellenmuster ist eine schlechte Verwendung von Schnittstellen. Dass eine Klasse einige Konstanten intern verwendet, ist ein Implementierungsdetail. Durch die Implementierung einer konstanten Schnittstelle wird dieses Implementierungsdetail in die exportierte API der Klasse übertragen. Für die Benutzer einer Klasse ist es nicht von Bedeutung, dass die Klasse eine konstante Schnittstelle implementiert. In der Tat kann es sie sogar verwirren. Schlimmer noch, es stellt eine Verpflichtung dar: Wenn die Klasse in einer zukünftigen Version so geändert wird, dass sie die Konstanten nicht mehr verwenden muss, muss sie dennoch die Schnittstelle implementieren, um die Binärkompatibilität sicherzustellen. Wenn eine nicht endgültige Klasse eine konstante Schnittstelle implementiert, werden alle Namespaces ihrer Unterklassen durch die Konstanten in der Schnittstelle verschmutzt.

Eine Aufzählung kann ein besserer Ansatz sein. Oder Sie können die Konstanten einfach als öffentliche statische Felder in eine Klasse einfügen, die nicht instanziiert werden kann. Dadurch kann eine andere Klasse auf sie zugreifen, ohne ihre eigene API zu verschmutzen.

Dan Dyer
quelle
8
Aufzählungen sind hier ein roter Hering - oder zumindest eine separate Frage. Aufzählungen sollten natürlich verwendet werden, aber sie sollten auch ausgeblendet werden, wenn sie von Implementierern nicht benötigt werden.
DJClayworth
12
Übrigens: Sie können eine Aufzählung ohne Instanzen als Klasse verwenden, die nicht instanziiert werden kann. ;)
Peter Lawrey
5
Aber warum überhaupt diese Schnittstellen implementieren? Warum nicht einfach als Konstanten-Repository verwenden? Wenn ich eine Art von Konstanten brauche, die global geteilt werden, sehe ich keine "saubereren" Möglichkeiten, dies zu tun.
Shadox
2
@ DanDyer ja, aber eine Schnittstelle macht einige Deklarationen implizit. Wie das öffentliche statische Finale ist nur ein Standard. Warum sich mit einer Klasse beschäftigen? Enum - nun, es kommt darauf an. Eine Aufzählung sollte eine Sammlung möglicher Werte für eine Entität definieren und keine Sammlung von Werten für verschiedene Entitäten.
Shadox
4
Persönlich habe ich das Gefühl, dass Josh den Ball falsch trifft. Wenn Sie nicht möchten, dass Ihre Konstanten durchlaufen - unabhängig davon, in welche Art von Objekt Sie sie einfügen - müssen Sie sicherstellen, dass sie NICHT Teil des exportierten Codes sind. Schnittstelle oder Klasse, beide können exportiert werden. Die richtige Frage lautet also nicht: In welchen Objekttyp stecke ich sie, sondern wie organisiere ich dieses Objekt? Wenn die Konstanten im exportierten Code verwendet werden, müssen Sie dennoch sicherstellen, dass sie nach dem Export verfügbar sind. Die Behauptung "schlechte Praxis" ist meiner bescheidenen Meinung nach ungültig.
Lawrence
99

Anstatt eine "Konstantenschnittstelle" zu implementieren, können Sie in Java 1.5+ statische Importe verwenden, um die Konstanten / statischen Methoden aus einer anderen Klasse / Schnittstelle zu importieren:

import static com.kittens.kittenpolisher.KittenConstants.*;

Dies vermeidet die Hässlichkeit, Ihre Klassen dazu zu bringen, Schnittstellen zu implementieren, die keine Funktionalität haben.

Ich denke, dass es manchmal notwendig ist, eine Klasse nur zum Speichern von Konstanten zu haben. Es gibt bestimmte Konstanten, die einfach keinen natürlichen Platz in einer Klasse haben, daher ist es besser, sie an einem "neutralen" Ort zu haben.

Verwenden Sie jedoch anstelle einer Schnittstelle eine letzte Klasse mit einem privaten Konstruktor. (Dies macht es unmöglich, die Klasse zu instanziieren oder zu unterklassifizieren, und sendet eine starke Nachricht, dass sie keine nicht statischen Funktionen / Daten enthält.)

Z.B:

/** Set of constants needed for Kitten Polisher. */
public final class KittenConstants
{
    private KittenConstants() {}

    public static final String KITTEN_SOUND = "meow";
    public static final double KITTEN_CUTENESS_FACTOR = 1;
}
Zarkonnen
quelle
Sie erklären also, dass wir aufgrund des statischen Imports Klassen anstelle von Schnittstellen verwenden sollten, um denselben Fehler wie zuvor erneut auszuführen?! Das ist dumm!
Gizmo
11
Nein, das sage ich überhaupt nicht. Ich sage zwei unabhängige Dinge. 1: Verwenden Sie statische Importe, anstatt die Vererbung zu missbrauchen. 2: Wenn Sie ein Konstanten-Repository benötigen, machen Sie es zu einer endgültigen Klasse anstelle einer Schnittstelle.
Zarkonnen
"Konstante Schnittstellen" wurden niemals als Teil einer Vererbung konzipiert, niemals. Der statische Import ist also nur für syntaktischen Zucker gedacht, und das Erben von einer solchen Schnittstelle ist ein schrecklicher Fehler. Ich weiß, dass Sun dies getan hat, aber sie haben auch Unmengen anderer grundlegender Fehler gemacht, das ist keine Entschuldigung, um sie nachzuahmen.
Gizmo
3
Eines der Probleme mit dem für die Frage veröffentlichten Code besteht darin, dass die Schnittstellenimplementierung nur verwendet wird, um den Zugriff auf Konstanten zu erleichtern. Wenn ich sehe, dass etwas FooInterface implementiert, erwarte ich, dass sich dies auf seine Funktionalität auswirkt, und das oben Gesagte verstößt dagegen. Statische Importe beheben dieses Problem.
Zarkonnen
2
gizmo - Ich bin kein Fan von statischen Importen, aber was er dort tut, ist zu vermeiden, den Klassennamen verwenden zu müssen, dh ConstClass.SOME_CONST. Wenn Sie einen statischen Import durchführen, werden diese Mitglieder nicht zu der Klasse hinzugefügt, die Z. von der Schnittstelle nicht erben soll. Er sagt tatsächlich das Gegenteil.
mtruesdell
8

Ich gebe nicht vor, richtig zu sein, aber sehen wir uns dieses kleine Beispiel an:

public interface CarConstants {

      static final String ENGINE = "mechanical";
      static final String WHEEL  = "round";
      // ...

}

public interface ToyotaCar extends CarConstants //, ICar, ... {
      void produce();
}

public interface FordCar extends CarConstants //, ICar, ... {
      void produce();
}

// and this is implementation #1
public class CamryCar implements ToyotaCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

// and this is implementation #2
public class MustangCar implements FordCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

ToyotaCar weiß nichts über FordCar und FordCar weiß nichts über ToyotaCar. Prinzip CarConstants sollten geändert werden, aber ...

Konstanten sollten nicht geändert werden, da das Rad rund und der Motor mechanisch ist, aber ... In Zukunft haben Toyotas Forschungsingenieure den elektronischen Motor und die platten Räder erfunden! Sehen wir uns unsere neue Oberfläche an

public interface InnovativeCarConstants {

          static final String ENGINE = "electronic";
          static final String WHEEL  = "flat";
          // ...
}

und jetzt können wir unsere Abstraktion ändern:

public interface ToyotaCar extends CarConstants

zu

public interface ToyotaCar extends InnovativeCarConstants 

Und jetzt, wenn wir jemals den Kernwert von ENGINE oder WHEEL ändern müssen, können wir die ToyotaCar-Schnittstelle auf Abstraktionsebene ändern, ohne Implementierungen zu berühren

Es ist NICHT SICHER, ich weiß, aber ich möchte immer noch wissen, dass Sie darüber nachdenken

Pleerock
quelle
Ich möchte Ihre Idee jetzt im Jahr 2019 kennenlernen. Für mich sollen Schnittstellenfelder von einigen Objekten gemeinsam genutzt werden.
Raining
Ich schrieb eine Antwort auf Ihre Idee bezogen werden : stackoverflow.com/a/55877115/5290519
Raining
Dies ist ein gutes Beispiel dafür, wie sehr die Regeln von PMD begrenzt sind. Der Versuch, durch Bürokratie besseren Code zu erhalten, bleibt ein vergeblicher Versuch.
Bebbo
6

Es gibt viel Hass für dieses Muster in Java. Eine Schnittstelle mit statischen Konstanten hat jedoch manchmal einen Wert. Grundsätzlich müssen Sie folgende Bedingungen erfüllen:

  1. Die Konzepte sind Teil der öffentlichen Schnittstelle mehrerer Klassen.

  2. Ihre Werte können sich in zukünftigen Versionen ändern.

  3. Es ist wichtig, dass alle Implementierungen dieselben Werte verwenden.

Angenommen, Sie schreiben eine Erweiterung für eine hypothetische Abfragesprache. In dieser Erweiterung erweitern Sie die Sprachsyntax um einige neue Operationen, die von einem Index unterstützt werden. Beispiel: Sie werden einen R-Tree haben, der Geodatenabfragen unterstützt.

Sie schreiben also eine öffentliche Schnittstelle mit der statischen Konstante:

public interface SyntaxExtensions {
     // query type
     String NEAR_TO_QUERY = "nearTo";

     // params for query
     String POINT = "coordinate";
     String DISTANCE_KM = "distanceInKm";
}

Jetzt später glaubt ein neuer Entwickler, er müsse einen besseren Index erstellen, also kommt er und erstellt eine R * -Implementierung. Durch die Implementierung dieser Schnittstelle in seinem neuen Baum garantiert er, dass die verschiedenen Indizes in der Abfragesprache dieselbe Syntax haben. Wenn Sie später entschieden haben, dass "nearTo" ein verwirrender Name ist, können Sie ihn in "insideDistanceInKm" ändern und wissen, dass die neue Syntax von allen Ihren Indeximplementierungen respektiert wird.

PS: Die Inspiration für dieses Beispiel stammt aus dem Neo4j-Raumcode.

phil_20686
quelle
5

Im Nachhinein können wir sehen, dass Java in vielerlei Hinsicht kaputt ist. Ein Hauptfehler von Java ist die Beschränkung der Schnittstellen auf abstrakte Methoden und statische Endfelder. Neuere, komplexere OO-Sprachen wie Scala fassen Schnittstellen nach Merkmalen zusammen, die konkrete Methoden enthalten können (und dies normalerweise auch tun), die Arität Null haben können (Konstanten!). Eine Darstellung von Merkmalen als Einheiten zusammensetzbaren Verhaltens finden Sie unter http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Eine kurze Beschreibung des Vergleichs von Merkmalen in Scala mit Schnittstellen in Java finden Sie unter http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. Im Zusammenhang mit dem Unterrichten von OO-Design sind vereinfachende Regeln wie die Behauptung, dass Schnittstellen niemals statische Felder enthalten sollten, dumm. Viele Merkmale enthalten natürlich Konstanten, und diese Konstanten sind angemessenerweise Teil der öffentlichen "Schnittstelle", die durch das Merkmal unterstützt wird. Beim Schreiben von Java-Code gibt es keine saubere und elegante Möglichkeit, Merkmale darzustellen. Die Verwendung statischer Endfelder innerhalb von Schnittstellen ist jedoch häufig Teil einer guten Problemumgehung.

Corky Cartwright
quelle
12
Schrecklich anmaßend und heutzutage veraltet.
Esko
1
Wunderbare Einsicht (+1), obwohl vielleicht etwas zu kritisch gegenüber Java.
Peter
0

Gemäß der JVM-Spezifikation können Felder und Methoden in einer Schnittstelle nur öffentlich, statisch, endgültig und abstrakt sein. Ref aus Inside Java VM

Standardmäßig sind alle Methoden in der Schnittstelle abstrakt, auch wenn Sie sie nicht explizit erwähnt haben.

Schnittstellen sollen nur Angaben machen. Es kann keine Implementierungen enthalten. Um zu vermeiden, dass Klassen implementiert werden, um die Spezifikation zu ändern, wird sie endgültig festgelegt. Da die Schnittstelle nicht instanziiert werden kann, werden sie statisch gemacht, um über den Schnittstellennamen auf das Feld zuzugreifen.

om39a
quelle
0

Ich habe nicht genug Ruf, um Pleerock einen Kommentar zu geben, deshalb muss ich eine Antwort erstellen. Das tut mir leid, aber er hat sich sehr bemüht und ich würde ihm gerne antworten.

Pleerock, Sie haben das perfekte Beispiel erstellt, um zu zeigen, warum diese Konstanten unabhängig von Schnittstellen und unabhängig von Vererbung sein sollten. Für den Kunden der Anwendung ist es nicht wichtig, dass es einen technischen Unterschied zwischen der Implementierung von Autos gibt. Sie sind für den Kunden gleich, nur für Autos. Der Client möchte sie also aus dieser Perspektive betrachten, die eine Schnittstelle wie I_Somecar darstellt. Während der gesamten Anwendung verwendet der Kunde nur eine Perspektive und nicht unterschiedliche für jede unterschiedliche Automarke.

Wenn ein Kunde Autos vor dem Kauf vergleichen möchte, kann er eine Methode wie die folgende anwenden:

public List<Decision> compareCars(List<I_Somecar> pCars);

Eine Schnittstelle ist ein Verhaltensvertrag und zeigt verschiedene Objekte aus einer Perspektive. So wie Sie es gestalten, hat jede Automarke ihre eigene Vererbungslinie. Obwohl es in Wirklichkeit ganz richtig ist, weil Autos so unterschiedlich sein können, dass es wie der Vergleich völlig unterschiedlicher Objekttypen sein kann, gibt es am Ende die Wahl zwischen verschiedenen Autos. Und das ist die Perspektive der Schnittstelle, die alle Marken teilen müssen. Die Wahl der Konstanten sollte dies nicht unmöglich machen. Bitte beachten Sie die Antwort von Zarkonnen.

Loek Bergman
quelle
-1

Dies kam aus einer Zeit, bevor Java 1.5 existiert und uns Aufzählungen bringt. Zuvor gab es keine gute Möglichkeit, eine Reihe von Konstanten oder eingeschränkten Werten zu definieren.

Dies wird immer noch verwendet, meistens entweder aus Gründen der Abwärtskompatibilität oder aufgrund der Menge an Refactoring, die erforderlich ist, um in vielen Projekten entfernt zu werden.

Gizmo
quelle
2
Vor Java 5 konnten Sie das typsichere Aufzählungsmuster verwenden (siehe java.sun.com/developer/Books/shiftintojava/page1.html ).
Dan Dyer