Interfaces con campos estáticos en java para compartir 'constantes'

116

Estoy mirando algunos proyectos Java de código abierto para ingresar a Java y noto que muchos de ellos tienen algún tipo de interfaz de 'constantes'.

Por ejemplo, processing.org tiene una interfaz llamada PConstants.java , y la mayoría de las otras clases principales implementan esta interfaz. La interfaz está plagada de miembros estáticos. ¿Hay alguna razón para este enfoque o se considera una mala práctica? ¿Por qué no usar enumeraciones donde tenga sentido , o una clase estática?

Me parece extraño usar una interfaz que permita algún tipo 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
fuente
15
Nota: static finalno es necesario, es redundante para una interfaz.
ThomasW
También tenga en cuenta que platformNamespuede haber public, staticy final, pero definitivamente no es una constante. La única matriz constante es una con longitud cero.
Vlasec
@ThomasW Sé que esto tiene algunos años, pero necesitaba señalar un error en tu comentario. static finalno es necesariamente redundante. Un campo de clase o interfaz con solo la finalpalabra clave crearía instancias separadas de ese campo a medida que crea objetos de la clase o interfaz. El uso static finalharía que cada objeto compartiera una ubicación de memoria para ese campo. En otras palabras, si una clase MyClass tuviera un campo final String str = "Hello";, para N instancias de MyClass, habría N instancias del campo str en la memoria. Agregar la staticpalabra clave daría como resultado una sola instancia.
Sintrias

Respuestas:

160

Generalmente se considera una mala práctica. El problema es que las constantes son parte de la "interfaz" pública (a falta de una palabra mejor) de la clase de implementación. Esto significa que la clase de implementación está publicando todos estos valores en clases externas incluso cuando solo se requieren internamente. Las constantes proliferan a lo largo del código. Un ejemplo es la interfaz SwingConstants en Swing, que es implementada por docenas de clases que todas "reexportan" todas sus constantes (incluso las que no usan) como propias.

Pero no solo confíe en mi palabra, Josh Bloch también dice que es malo:

El patrón de interfaz constante es un mal uso de las interfaces. Que una clase use algunas constantes internamente es un detalle de implementación. La implementación de una interfaz constante hace que este detalle de implementación se filtre en la API exportada de la clase. No tiene ninguna importancia para los usuarios de una clase que la clase implemente una interfaz constante. De hecho, incluso puede confundirlos. Peor aún, representa un compromiso: si en una versión futura se modifica la clase para que ya no necesite usar las constantes, aún debe implementar la interfaz para asegurar la compatibilidad binaria. Si una clase no final implementa una interfaz constante, todas sus subclases tendrán sus espacios de nombres contaminados por las constantes en la interfaz.

Una enumeración puede ser un mejor enfoque. O simplemente podría poner las constantes como campos estáticos públicos en una clase de la que no se puede crear una instancia. Esto permite que otra clase acceda a ellos sin contaminar su propia API.

Dan Dyer
fuente
8
Las enumeraciones son una pista falsa aquí, o al menos una pregunta separada. Por supuesto, deben usarse enumeraciones, pero también deben ocultarse si los implementadores no las necesitan.
DJClayworth
12
Por cierto: puede usar una enumeración sin instancias como una clase que no se puede instanciar. ;)
Peter Lawrey
5
Pero, ¿por qué implementar esas interfaces en primer lugar? ¿Por qué no usarlos solo como repositorio de constantes? Si necesito algún tipo de constantes compartidas globalmente, no veo formas "más limpias" de hacerlo.
shadox
2
@DanDyer sí, pero una interfaz hace algunas declaraciones implícitas. Como public static final es solo un valor predeterminado. ¿Por qué molestarse con una clase? Enum - bueno, depende. Una enumeración debe definir una colección de valores posibles para una entidad y no una colección de valores para diferentes entidades.
shadox
4
Personalmente, siento que Josh está golpeando mal la pelota. Si no desea que sus constantes se filtren, independientemente del tipo de objeto que las coloque, debe asegurarse de que NO formen parte del código exportado. Interfaz o clase, ambos se pueden exportar. Entonces, la pregunta correcta no es: en qué tipo de objeto los coloco, sino cómo organizo este objeto. Y si las constantes se utilizan en el código exportado, aún debe asegurarse de que estén disponibles una vez exportadas. Así que la afirmación de "malas prácticas" es, en mi humilde opinión, inválida.
Lawrence
99

En lugar de implementar una "interfaz de constantes", en Java 1.5+, puede usar importaciones estáticas para importar las constantes / métodos estáticos de otra clase / interfaz:

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

Esto evita la fealdad de hacer que sus clases implementen interfaces que no tienen funcionalidad.

En cuanto a la práctica de tener una clase solo para almacenar constantes, creo que a veces es necesario. Hay ciertas constantes que simplemente no tienen un lugar natural en una clase, por lo que es mejor tenerlas en un lugar "neutral".

Pero en lugar de usar una interfaz, use una clase final con un constructor privado. (Haciendo imposible crear instancias o subclases de la clase, enviando un fuerte mensaje de que no contiene funcionalidad / datos no estáticos).

P.ej:

/** 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
fuente
Entonces, ¿estás explicando que, debido a la importación estática, deberíamos usar clases en lugar de interfaces para repetir el mismo error que antes? ¡Eso es tonto!
gizmo
11
No, eso no es lo que estoy diciendo en absoluto. Estoy diciendo dos cosas independientes. 1: use importaciones estáticas en lugar de abusar de la herencia. 2: Si debe tener un repositorio de constantes, conviértalo en una clase final en lugar de una interfaz.
Zarkonnen
Las "interfaces constantes" no estaban diseñadas para ser parte de ninguna herencia, nunca. Entonces, la importación estática es solo para azúcar sintáctica, y heredar de dicha interfaz es un error horrible. Sé que Sun hizo esto, pero también cometió muchos otros errores básicos, esa no es una excusa para imitarlos.
gizmo
3
Uno de los problemas con el código publicado para la pregunta es que la implementación de la interfaz se usa solo para obtener un acceso más fácil a las constantes. Cuando veo que algo implementa FooInterface, espero que afecte su funcionalidad, y lo anterior viola esto. Las importaciones estáticas solucionan ese problema.
Zarkonnen
2
gizmo: no soy un fanático de las importaciones estáticas, pero lo que está haciendo allí es evitar tener que usar el nombre de la clase, es decir, ConstClass.SOME_CONST. Hacer una importación estática no agrega esos miembros a la clase que Z. no dice que herede de la interfaz, de hecho dice lo contrario.
mtruesdell
8

No pretendo tener razón, pero veamos este pequeño ejemplo:

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 no sabe nada sobre FordCar y FordCar no sabe nada sobre ToyotaCar. El principio CarConstants debería cambiarse, pero ...

Las constantes no deben cambiarse, porque la rueda es redonda y el motor es mecánico, pero ... ¡En el futuro, los ingenieros de investigación de Toyota inventaron el motor electrónico y las ruedas planas! Veamos nuestra nueva interfaz

public interface InnovativeCarConstants {

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

y ahora podemos cambiar nuestra abstracción:

public interface ToyotaCar extends CarConstants

a

public interface ToyotaCar extends InnovativeCarConstants 

Y ahora, si alguna vez necesitamos cambiar el valor central, si el MOTOR o la RUEDA, podemos cambiar la interfaz ToyotaCar en el nivel de abstracción, no toque las implementaciones.

NO ES SEGURO, lo sé, pero aún quiero saber si piensas en esto

pleerock
fuente
Quiero conocer tu idea ahora en 2019. Para mí, los campos de la interfaz están destinados a ser compartidos entre algunos objetos.
Lloviendo el
Escribí una respuesta relacionada con su idea: stackoverflow.com/a/55877115/5290519
Lloviendo
este es un buen ejemplo de cómo las reglas de PMD son muy limitadas. Tratar de obtener un mejor código mediante la burocracia sigue siendo un intento inútil.
bebbo
6

Hay mucho odio por este patrón en Java. Sin embargo, una interfaz de constantes estáticas a veces tiene valor. Básicamente, debes cumplir las siguientes condiciones:

  1. Los conceptos forman parte de la interfaz pública de varias clases.

  2. Sus valores pueden cambiar en versiones futuras.

  3. Es fundamental que todas las implementaciones utilicen los mismos valores.

Por ejemplo, suponga que está escribiendo una extensión para un lenguaje de consulta hipotético. En esta extensión vas a expandir la sintaxis del lenguaje con algunas operaciones nuevas, que son soportadas por un índice. Por ejemplo, tendrá un R-Tree que soporte consultas geoespaciales.

Entonces escribe una interfaz pública con la constante estática:

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

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

Ahora, más tarde, un nuevo desarrollador piensa que necesita crear un índice mejor, por lo que viene y crea una implementación R *. Al implementar esta interfaz en su nuevo árbol, garantiza que los diferentes índices tendrán la misma sintaxis en el lenguaje de consulta. Además, si más tarde decidió que "nearTo" era un nombre confuso, podría cambiarlo a "withinDistanceInKm" y saber que la nueva sintaxis sería respetada por todas sus implementaciones de índices.

PD: La inspiración para este ejemplo se extrae del código espacial de Neo4j.

phil_20686
fuente
5

Dada la ventaja de la retrospectiva, podemos ver que Java está roto de muchas maneras. Una falla importante de Java es la restricción de interfaces a métodos abstractos y campos finales estáticos. Los lenguajes OO más nuevos y sofisticados, como Scala, subsumen interfaces por rasgos que pueden (y normalmente lo hacen) incluir métodos concretos, que pueden tener arity cero (¡constantes!). Para obtener una exposición sobre los rasgos como unidades de comportamiento componible, consulte http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Para obtener una breve descripción de cómo se comparan los rasgos en Scala con las interfaces en Java, consulte http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. En el contexto de la enseñanza del diseño de OO, las reglas simplistas como afirmar que las interfaces nunca deben incluir campos estáticos son tontas. Muchos rasgos incluyen naturalmente constantes y estas constantes son apropiadamente parte de la "interfaz" pública soportada por el rasgo. Al escribir código Java, no existe una forma limpia y elegante de representar rasgos, pero el uso de campos finales estáticos dentro de las interfaces suele ser parte de una buena solución.

Corky Cartwright
fuente
12
Terriblemente pretencioso y hoy en día anticuado.
Esko
1
Maravillosa información (+1), aunque quizás un poco demasiado crítica contra Java.
peterh - Reincorpora a Monica
0

De acuerdo con la especificación de JVM, los campos y métodos en una interfaz solo pueden tener Público, Estático, Final y Resumen. Referencia desde dentro de Java VM

De forma predeterminada, todos los métodos en la interfaz son abstractos, incluso aunque no lo hayas mencionado explícitamente.

Las interfaces están destinadas a proporcionar solo especificaciones. No puede contener implementaciones. Entonces, para evitar implementar clases para cambiar la especificación, se hace final. Dado que no se puede crear una instancia de Interface, se vuelven estáticos para acceder al campo usando el nombre de la interfaz.

om39a
fuente
0

No tengo la reputación suficiente para dar un comentario a Pleerock, por lo que tengo que crear una respuesta. Lo siento, pero se esforzó mucho y me gustaría responderle.

Pleerock, creó el ejemplo perfecto para mostrar por qué esas constantes deberían ser independientes de las interfaces e independientes de la herencia. Para el cliente de la aplicación no es importante que exista una diferencia técnica entre la implementación de los automóviles. Son lo mismo para el cliente, solo coches. Entonces, el cliente quiere verlos desde esa perspectiva, que es una interfaz como I_Somecar. A lo largo de la aplicación, el cliente utilizará solo una perspectiva y no diferentes para cada marca de automóvil diferente.

Si un cliente quiere comparar autos antes de comprar, puede tener un método como este:

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

Una interfaz es un contrato sobre el comportamiento y muestra diferentes objetos desde una perspectiva. La forma en que lo diseñe, ¿tendrá cada marca de automóvil su propia línea de herencia? Aunque en realidad es bastante correcto, porque los autos pueden ser tan diferentes que puede ser como comparar tipos de objetos completamente diferentes, al final hay que elegir entre diferentes autos. Y esa es la perspectiva de la interfaz que todas las marcas deben compartir. La elección de constantes no debería hacer esto imposible. Por favor, considere la respuesta de Zarkonnen.

Loek Bergman
fuente
-1

Esto vino de una época anterior a la existencia de Java 1.5 y nos trajo enumeraciones. Antes de eso, no había una buena forma de definir un conjunto de constantes o valores restringidos.

Esto todavía se usa, la mayoría de las veces, ya sea por compatibilidad con versiones anteriores o debido a la cantidad de refactorización necesaria para deshacerse, en muchos proyectos.

artilugio
fuente
2
Antes de Java 5, podía utilizar el patrón de enumeración de tipo seguro (consulte java.sun.com/developer/Books/shiftintojava/page1.html ).
Dan Dyer