Interfaces avec des champs statiques en java pour partager des 'constantes'

116

Je regarde quelques projets Java open source pour entrer dans Java et je remarque que beaucoup d'entre eux ont une sorte d'interface «constantes».

Par exemple, processing.org a une interface appelée PConstants.java , et la plupart des autres classes de base implémentent cette interface. L'interface est criblée de membres statiques. Y a-t-il une raison à cette approche ou est-ce considéré comme une mauvaise pratique? Pourquoi ne pas utiliser des énumérations là où cela a du sens , ou une classe statique?

Je trouve étrange d'utiliser une interface pour permettre une sorte de pseudo «variables globales».

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
la source
15
Remarque: static finaln'est pas nécessaire, il est redondant pour une interface.
ThomasW
Notez également que cela platformNamespeut être public, staticet final, mais ce n'est certainement pas une constante. Le seul tableau constant est un tableau de longueur nulle.
Vlasec
@ThomasW Je sais que cela date de quelques années, mais j'avais besoin de signaler une erreur dans votre commentaire. static finaln'est pas nécessairement redondant. Un champ de classe ou d'interface avec uniquement le finalmot - clé créerait des instances distinctes de ce champ lorsque vous créez des objets de la classe ou de l'interface. Utiliser static finalferait en sorte que chaque objet partage un emplacement mémoire pour ce champ. En d'autres termes, si une classe MyClass avait un champ final String str = "Hello";, pour N instances de MyClass, il y aurait N instances du champ str en mémoire. L'ajout du staticmot clé ne donnerait qu'une seule instance.
Sintrias

Réponses:

160

C'est généralement considéré comme une mauvaise pratique. Le problème est que les constantes font partie de "l'interface" publique (faute d'un meilleur mot) de la classe d'implémentation. Cela signifie que la classe d'implémentation publie toutes ces valeurs dans des classes externes, même lorsqu'elles ne sont requises qu'en interne. Les constantes prolifèrent dans tout le code. Un exemple est l' interface SwingConstants dans Swing, qui est implémentée par des dizaines de classes qui «réexportent» toutes ses constantes (même celles qu'elles n'utilisent pas) comme les leurs.

Mais ne me croyez pas sur parole, Josh Bloch dit aussi que c'est mauvais:

Le modèle d'interface constant est une mauvaise utilisation des interfaces. Le fait qu'une classe utilise certaines constantes en interne est un détail d'implémentation. L'implémentation d'une interface constante entraîne la fuite de ce détail d'implémentation dans l'API exportée de la classe. Il est sans conséquence pour les utilisateurs d'une classe que la classe implémente une interface constante. En fait, cela peut même les confondre. Pire encore, cela représente un engagement: si dans une prochaine version la classe est modifiée pour ne plus avoir besoin d'utiliser les constantes, elle doit quand même implémenter l'interface pour assurer la compatibilité binaire. Si une classe non finale implémente une interface constante, toutes ses sous-classes verront leurs espaces de noms pollués par les constantes de l'interface.

Une énumération peut être une meilleure approche. Ou vous pouvez simplement placer les constantes sous forme de champs statiques publics dans une classe qui ne peut pas être instanciée. Cela permet à une autre classe d'y accéder sans polluer sa propre API.

Dan Dyer
la source
8
Les énumérations sont ici un hareng rouge - ou du moins une question distincte. Les énumérations doivent bien sûr être utilisées, mais elles doivent également être masquées si elles ne sont pas nécessaires aux implémenteurs.
DJClayworth
12
BTW: Vous pouvez utiliser une énumération sans instances comme classe qui ne peut pas être instanciée. ;)
Peter Lawrey
5
Mais pourquoi mettre en œuvre ces interfaces en premier lieu? Pourquoi ne pas les utiliser comme référentiel de constantes? Si j'ai besoin d'une sorte de constantes partagées globalement, je ne vois pas de moyens "plus propres" de le faire.
shadox
2
@DanDyer ouais, mais une interface rend certaines déclarations implicites. Comme le public static final n'est qu'une valeur par défaut. Pourquoi s'embêter avec une classe? Enum - eh bien, cela dépend. Une énumération doit définir une collection de valeurs possibles pour une entité et non une collection de valeurs pour différentes entités.
shadox
4
Personnellement, je sens que Josh frappe mal la balle. Si vous ne voulez pas que vos constantes fuient - quel que soit le type d'objet que vous les placez - vous devez vous assurer qu'elles ne font PAS partie du code exporté. Interface ou classe, les deux peuvent être exportés. La bonne question à se poser n'est donc pas: dans quel type d'objet je les mets mais comment organiser cet objet. Et si les constantes sont utilisées dans le code exporté, vous devez toujours vous assurer qu'elles sont disponibles une fois exportées. Donc, l'allégation de «mauvaise pratique» est, à mon humble avis, invalide.
Lawrence
99

Au lieu d'implémenter une "interface de constantes", dans Java 1.5+, vous pouvez utiliser des importations statiques pour importer les constantes / méthodes statiques d'une autre classe / interface:

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

Cela évite la laideur de faire en sorte que vos classes implémentent des interfaces qui n'ont aucune fonctionnalité.

Quant à la pratique d'avoir une classe juste pour stocker des constantes, je pense que c'est parfois nécessaire. Il y a certaines constantes qui n'ont tout simplement pas de place naturelle dans une classe, il est donc préférable de les avoir dans un endroit «neutre».

Mais au lieu d'utiliser une interface, utilisez une classe finale avec un constructeur privé. (Rendant impossible l'instanciation ou la sous-classe de la classe, l'envoi d'un message fort indiquant qu'elle ne contient pas de fonctionnalités / données non statiques.)

Par exemple:

/** 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
la source
Donc, vous expliquez que, à cause de l'importation statique, nous devrions utiliser des classes au lieu des entières pour refaire la même erreur qu'avant?! Cela est bête!
gizmo
11
Non, ce n'est pas du tout ce que je dis. Je dis deux choses indépendantes. 1: utilisez les importations statiques au lieu d'abuser de l'héritage. 2: Si vous devez avoir un référentiel de constantes, faites-en une classe finale au lieu d'une interface.
Zarkonnen
«Interfaces constantes» où elles ne sont pas conçues pour faire partie d'un héritage, jamais. Donc, l'importation statique est juste pour le sucre syntaxique, et hériter d'une telle interface est une horrible erreur. Je sais que Sun a fait cela, mais ils ont également commis des tonnes d'autres erreurs de base, ce n'est pas une excuse pour les imiter.
gizmo
3
L'un des problèmes avec le code publié pour la question est que l'implémentation de l'interface est utilisée uniquement pour accéder plus facilement aux constantes. Quand je vois quelque chose implémenter FooInterface, je m'attends à ce que cela ait un impact sur sa fonctionnalité, et ce qui précède enfreint cela. Les importations statiques résolvent ce problème.
Zarkonnen
2
gizmo - Je ne suis pas fan des importations statiques, mais ce qu'il fait là-bas, c'est d'éviter d'avoir à utiliser le nom de la classe, c'est-à-dire ConstClass.SOME_CONST. Faire une importation statique n'ajoute pas ces membres à la classe que vous Z. ne dit pas d'hériter de l'interface, il dit le contraire en fait.
mtruesdell
8

Je ne prétends pas avoir raison, mais voyons ce petit exemple:

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 ne sait rien de FordCar, et FordCar ne sait rien de ToyotaCar. principe CarConstants doit être changé, mais ...

Les constantes ne doivent pas être changées, car la roue est ronde et l'égine est mécanique, mais ... À l'avenir, les ingénieurs de recherche de Toyota ont inventé le moteur électronique et les roues plates! Voyons notre nouvelle interface

public interface InnovativeCarConstants {

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

et maintenant nous pouvons changer notre abstraction:

public interface ToyotaCar extends CarConstants

à

public interface ToyotaCar extends InnovativeCarConstants 

Et maintenant, si jamais nous avons besoin de changer la valeur fondamentale du MOTEUR ou de la ROUE, nous pouvons changer l'interface ToyotaCar au niveau de l'abstraction, ne touchez pas aux implémentations

Ce n'est PAS SÛR, je sais, mais je veux quand même savoir que pensez-vous de ça

pleerock
la source
Je veux connaître votre idée maintenant en 2019. Pour moi, les champs d'interface sont destinés à être partagés entre certains objets.
pleut le
J'ai écrit une réponse en rapport avec votre idée: stackoverflow.com/a/55877115/5290519
pleut le
c'est un bon exemple de la manière dont les règles de PMD sont très limitées. Essayer d'obtenir un meilleur code grâce à la bureaucratie reste une tentative vaine.
bebbo
6

Il y a beaucoup de haine pour ce modèle en Java. Cependant, une interface de constantes statiques a parfois de la valeur. Vous devez essentiellement remplir les conditions suivantes:

  1. Les concepts font partie de l'interface publique de plusieurs classes.

  2. Leurs valeurs pourraient changer dans les versions futures.

  3. Il est essentiel que toutes les implémentations utilisent les mêmes valeurs.

Par exemple, supposons que vous écrivez une extension dans un langage de requête hypothétique. Dans cette extension, vous allez développer la syntaxe du langage avec de nouvelles opérations, qui sont prises en charge par un index. Par exemple, vous allez avoir un R-Tree prenant en charge les requêtes géospatiales.

Vous écrivez donc une interface publique avec la constante statique:

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

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

Plus tard, un nouveau développeur pense qu'il a besoin de créer un meilleur index, alors il vient et construit une implémentation R *. En implémentant cette interface dans sa nouvelle arborescence, il garantit que les différents index auront une syntaxe identique dans le langage de requête. De plus, si vous décidiez plus tard que "nearTo" était un nom déroutant, vous pourriez le changer en "withinDistanceInKm" et savoir que la nouvelle syntaxe serait respectée par toutes vos implémentations d'index.

PS: L'inspiration de cet exemple est tirée du code spatial Neo4j.

phil_20686
la source
5

Compte tenu de l'avantage du recul, nous pouvons voir que Java est cassé à bien des égards. Un échec majeur de Java est la restriction des interfaces aux méthodes abstraites et aux champs finaux statiques. Les langages OO plus récents et plus sophistiqués comme Scala subsument les interfaces par des traits qui peuvent (et le font généralement) inclure des méthodes concrètes, qui peuvent avoir une arité nulle (constantes!). Pour une exposition sur les traits en tant qu'unités de comportement composable, voir http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Pour une brève description de la façon dont les traits de Scala se comparent aux interfaces de Java, voir http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. Dans le contexte de l'enseignement de la conception OO, des règles simplistes comme affirmer que les interfaces ne devraient jamais inclure de champs statiques sont stupides. De nombreux traits incluent naturellement des constantes et ces constantes font de façon appropriée partie de «l'interface» publique supportée par le trait. Lors de l'écriture de code Java, il n'y a pas de moyen propre et élégant de représenter les traits, mais l'utilisation de champs finaux statiques dans les interfaces fait souvent partie d'une bonne solution de contournement.

Corky Cartwright
la source
12
Terriblement prétentieux et aujourd'hui dépassé.
Esko
1
Un aperçu merveilleux (+1), bien que peut-être un peu trop critique contre Java.
peterh - Réintégrer Monica
0

Selon la spécification JVM, les champs et les méthodes d'une interface ne peuvent avoir que Public, Static, Final et Abstract. Réf de Inside Java VM

Par défaut, toutes les méthodes de l'interface sont abstraites même si vous ne les avez pas mentionnées explicitement.

Les interfaces sont destinées à ne donner que des spécifications. Il ne peut contenir aucune implémentation. Donc, pour éviter d'implémenter des classes pour changer la spécification, elle est rendue définitive. Puisque l'interface ne peut pas être instanciée, elles sont rendues statiques pour accéder au champ en utilisant le nom de l'interface.

om39a
la source
0

Je n'ai pas assez de réputation pour faire un commentaire à Pleerock, je dois donc créer une réponse. Je suis désolé pour cela, mais il y a fait de gros efforts et je voudrais lui répondre.

Pleerock, vous avez créé l'exemple parfait pour montrer pourquoi ces constantes devraient être indépendantes des interfaces et indépendantes de l'héritage. Pour le client de l'application, il n'est pas important qu'il y ait une différence technique entre ces implémentations de voitures. Ce sont les mêmes pour le client, juste des voitures. Donc, le client veut les regarder de cette perspective, qui est une interface comme I_Somecar. Tout au long de l'application, le client n'utilisera qu'une seule perspective et non des perspectives différentes pour chaque marque de voiture.

Si un client souhaite comparer des voitures avant d'acheter, il peut avoir une méthode comme celle-ci:

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

Une interface est un contrat sur le comportement et montre différents objets d'un point de vue. La façon dont vous la concevez, chaque marque automobile aura sa propre ligne d'héritage. Bien que ce soit en réalité tout à fait correct, parce que les voitures peuvent être si différentes que cela peut être comme comparer des types d'objets complètement différents, à la fin, il y a le choix entre différentes voitures. Et c'est la perspective de l'interface que toutes les marques doivent partager. Le choix des constantes ne doit pas rendre cela impossible. Veuillez considérer la réponse de Zarkonnen.

Loek Bergman
la source
-1

Cela est venu d'un temps avant que Java 1.5 existe et nous apporte des énumérations. Auparavant, il n'y avait pas de bon moyen de définir un ensemble de constantes ou de valeurs contraintes.

Ceci est toujours utilisé, la plupart du temps soit pour la compatibilité ascendante, soit en raison de la quantité de refactoring nécessaire pour se débarrasser, dans de nombreux projets.

gadget
la source
2
Avant Java 5, vous pouviez utiliser le modèle d'énumération de type sécurisé (voir java.sun.com/developer/Books/shiftintojava/page1.html ).
Dan Dyer