Interfacce con campi statici in java per la condivisione di "costanti"


116

Sto esaminando alcuni progetti Java open source per entrare in Java e noto che molti di loro hanno una sorta di interfaccia "costanti".

Ad esempio, processing.org ha un'interfaccia chiamata PConstants.java e la maggior parte delle altre classi principali implementa questa interfaccia. L'interfaccia è piena di membri statici. C'è una ragione per questo approccio o è considerata una cattiva pratica? Perché non usare le enumerazioni dove ha senso o una classe statica?

Trovo strano usare un'interfaccia per consentire una sorta di pseudo "variabili globali".

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

}

15
Nota: static finalnon è necessario, è ridondante per un'interfaccia.
ThomasW

Nota anche che platformNamespotrebbe essere public, statice final, ma sicuramente non è una costante. L'unico array costante è uno con lunghezza zero.
Vlasec

@ThomasW So che ha qualche anno, ma dovevo segnalare un errore nel tuo commento. static finalnon è necessariamente ridondante. Un campo classe o interfaccia con solo la finalparola chiave creerebbe istanze separate di quel campo mentre si creano oggetti della classe o dell'interfaccia. L'utilizzo static finalfarebbe sì che ogni oggetto condivida una posizione di memoria per quel campo. In altre parole, se una classe MyClass avesse un campo final String str = "Hello";, per N istanze di MyClass, ci sarebbero N istanze del campo str in memoria. L'aggiunta della staticparola chiave comporterebbe solo 1 istanza.
Sintrias

Risposte:


160

È generalmente considerata una cattiva pratica. Il problema è che le costanti fanno parte dell '"interfaccia" pubblica (in mancanza di una parola migliore) della classe di implementazione. Ciò significa che la classe di implementazione pubblica tutti questi valori in classi esterne anche quando sono richiesti solo internamente. Le costanti proliferano in tutto il codice. Un esempio è l' interfaccia SwingConstants in Swing, che è implementata da dozzine di classi che "riesportano" tutte le sue costanti (anche quelle che non usano) come proprie.

Ma non fidarti solo della mia parola, Josh Bloch dice anche che è un male:

Il modello di interfaccia costante è un cattivo utilizzo delle interfacce. Che una classe utilizzi alcune costanti internamente è un dettaglio di implementazione. L'implementazione di un'interfaccia costante fa sì che questo dettaglio di implementazione trapeli nell'API esportata della classe. Non ha importanza per gli utenti di una classe che la classe implementi un'interfaccia costante. In effetti, potrebbe persino confonderli. Peggio ancora, rappresenta un impegno: se in una futura release la classe viene modificata in modo da non dover più utilizzare le costanti, deve comunque implementare l'interfaccia per garantire la compatibilità binaria. Se una classe non finale implementa un'interfaccia costante, tutte le sue sottoclassi avranno i loro spazi dei nomi inquinati dalle costanti nell'interfaccia.

Un enum può essere un approccio migliore. Oppure potresti semplicemente mettere le costanti come campi statici pubblici in una classe che non può essere istanziata. Ciò consente a un'altra classe di accedervi senza inquinare la propria API.


8
Gli enum qui sono una falsa pista, o almeno una domanda separata. Gli enum dovrebbero ovviamente essere usati, ma dovrebbero anche essere nascosti se non sono necessari agli implementatori.
DJClayworth

12
BTW: puoi usare un'enumerazione senza istanze come classe che non può essere istanziata. ;)
Peter Lawrey

5
Ma perché implementare queste interfacce in primo luogo? Perché non usarli solo come repository di costanti? Se ho bisogno di un qualche tipo di costanti condivise a livello globale, non vedo modi "più puliti" per farlo.
shadox

2
@ DanDyer sì, ma un'interfaccia rende implicite alcune dichiarazioni. Come il finale statico pubblico è solo un'impostazione predefinita. Perché perdere tempo con una lezione? Enum - beh, dipende. Un enum dovrebbe definire una raccolta di valori possibili per un'entità e non una raccolta di valori per entità diverse.
Shadox

4
Personalmente sento che Josh sta colpendo la palla male. Se non vuoi che le tue costanti trapelino, indipendentemente dal tipo di oggetto che le metti, devi assicurarti che NON facciano parte del codice esportato. Interfaccia o classe, entrambi possono essere esportati. Quindi la domanda giusta da porsi non è: in che tipo di oggetto li metto ma come organizzo questo oggetto. E se le costanti vengono utilizzate nel codice esportato, devi comunque assicurarti che siano disponibili una volta esportate. Quindi l'affermazione "cattiva pratica" è a mio modesto parere non valida.
Lawrence

99

Invece di implementare una "interfaccia delle costanti", in Java 1.5+, è possibile utilizzare importazioni statiche per importare le costanti / metodi statici da un'altra classe / interfaccia:

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

Questo evita la bruttezza di fare in modo che le tue classi implementino interfacce che non hanno funzionalità.

Per quanto riguarda la pratica di avere una classe solo per memorizzare le costanti, penso che a volte sia necessario. Ci sono alcune costanti che semplicemente non hanno un posto naturale in una classe, quindi è meglio averle in un posto "neutro".

Ma invece di usare un'interfaccia, usa una classe finale con un costruttore privato. (Rendendo impossibile istanziare o sottoclassare la classe, inviando un messaggio forte che non contiene funzionalità / dati non statici.)

Per esempio:

/** 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;
}

Quindi, stai spiegando che, a causa dell'importazione statica, dovremmo usare classi invece di intefaces per ripetere lo stesso errore di prima ?! È stupido!
gizmo

11
No, non è affatto quello che sto dicendo. Sto dicendo due cose indipendenti. 1: usa le importazioni statiche invece di abusare dell'ereditarietà. 2: Se devi avere un repository di costanti, rendilo una classe finale invece di un'interfaccia.
Zarkonnen

"Interfacce costanti" dove non sono progettate per essere parte di alcuna eredità, mai. Quindi l'importazione statica è solo per lo zucchero sintattico, ed ereditare da tale interfaccia è un errore orribile. So che Sun ha fatto questo, ma hanno anche commesso un sacco di altri errori di base, non è una scusa per imitarli.
gizmo

3
Uno dei problemi con il codice pubblicato per la domanda è che l'implementazione dell'interfaccia viene utilizzata solo per ottenere un accesso più semplice alle costanti. Quando vedo qualcosa che implementa FooInterface, mi aspetto che abbia un impatto sulla sua funzionalità, e quanto sopra viola questo. Le importazioni statiche risolvono questo problema.
Zarkonnen

2
gizmo - Non sono un fan delle importazioni statiche, ma quello che sta facendo lì è evitare di dover usare il nome della classe, cioè ConstClass.SOME_CONST. L'esecuzione di un'importazione statica non aggiunge quei membri alla classe che Z. non dice di ereditare dall'interfaccia, anzi dice il contrario.
mtruesdell

8

Non pretendo il diritto di avere ragione, ma vediamo questo piccolo esempio:

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 non sa nulla di FordCar e FordCar non sa di ToyotaCar. principio CarConstants dovrebbe essere cambiato, ma ...

Le costanti non dovrebbero essere cambiate, perché la ruota è rotonda e il motore è meccanico, ma ... In futuro i ricercatori Toyota hanno inventato il motore elettronico e le ruote piatte! Vediamo la nostra nuova interfaccia

public interface InnovativeCarConstants {

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

e ora possiamo cambiare la nostra astrazione:

public interface ToyotaCar extends CarConstants

per

public interface ToyotaCar extends InnovativeCarConstants 

E ora, se mai avremo bisogno di cambiare il valore fondamentale se il MOTORE o la RUOTA possiamo cambiare l'interfaccia ToyotaCar a livello di astrazione, non toccare le implementazioni

Non è SICURO, lo so, ma voglio comunque sapere che ne pensi


Voglio sapere la tua idea ora nel 2019. Per me i campi dell'interfaccia sono pensati per essere condivisi tra alcuni oggetti.
Pioggia il

Ho scritto una risposta relativa alla tua idea: stackoverflow.com/a/55877115/5290519
Raining

questo è un buon esempio di come le regole di PMD siano molto limitate. Cercare di ottenere un codice migliore attraverso la burocrazia rimane un tentativo inutile.
bebbo

6

C'è molto odio per questo modello in Java. Tuttavia, un'interfaccia di costanti statiche a volte ha valore. Fondamentalmente devi soddisfare le seguenti condizioni:

  1. I concetti fanno parte dell'interfaccia pubblica di diverse classi.

  2. I loro valori potrebbero cambiare nelle versioni future.

  3. È fondamentale che tutte le implementazioni utilizzino gli stessi valori.

Si supponga, ad esempio, di scrivere un'estensione per un ipotetico linguaggio di query. In questa estensione espanderai la sintassi del linguaggio con alcune nuove operazioni, che sono supportate da un indice. Ad esempio, avrai un R-Tree che supporta le query geospaziali.

Quindi scrivi un'interfaccia pubblica con la costante statica:

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

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

In seguito, un nuovo sviluppatore pensa di aver bisogno di costruire un indice migliore, quindi arriva e crea un'implementazione R *. Implementando questa interfaccia nel suo nuovo albero, garantisce che i diversi indici avranno la sintassi identica nel linguaggio di query. Inoltre, se in seguito decidessi che "nearTo" era un nome confuso, potresti cambiarlo in "withinDistanceInKm" e sapere che la nuova sintassi sarebbe rispettata da tutte le implementazioni dell'indice.

PS: L'ispirazione per questo esempio è tratta dal codice spaziale di Neo4j.


5

Dato il vantaggio del senno di poi, possiamo vedere che Java è rotto in molti modi. Uno dei principali difetti di Java è la restrizione delle interfacce ai metodi astratti e ai campi finali statici. Linguaggi OO più recenti e sofisticati come Scala sussume le interfacce in base a tratti che possono (e tipicamente lo fanno) includere metodi concreti, che possono avere arità zero (costanti!). Per un'esposizione sui tratti come unità di comportamento componibile, vedere http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Per una breve descrizione di come i tratti in Scala si confrontano con le interfacce in Java, vedere http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. Nel contesto dell'insegnamento del design OO, regole semplicistiche come affermare che le interfacce non dovrebbero mai includere campi statici sono sciocche. Molti tratti includono naturalmente le costanti e queste costanti sono appropriatamente parte dell '"interfaccia" pubblica supportata dal tratto. Nello scrivere codice Java, non esiste un modo pulito ed elegante per rappresentare i tratti, ma l'uso di campi finali statici all'interno delle interfacce è spesso parte di una buona soluzione alternativa.


12
Terribilmente pretenzioso e oggigiorno obsoleto.
Esko

1
Ottima intuizione (+1), anche se forse un po 'troppo critica nei confronti di Java.
peterh - Ripristina Monica il

0

Secondo la specifica JVM, i campi e i metodi in un'interfaccia possono avere solo Pubblico, Statico, Finale e Astratto. Rif da Inside Java VM

Per impostazione predefinita, tutti i metodi nell'interfaccia sono astratti anche se non li hai menzionati esplicitamente.

Le interfacce hanno lo scopo di fornire solo specifiche. Non può contenere implementazioni. Quindi, per evitare di implementare classi per modificare la specifica, viene resa definitiva. Poiché l'interfaccia non può essere istanziata, vengono rese statiche per accedere al campo utilizzando il nome dell'interfaccia.


0

Non ho abbastanza reputazione per dare un commento a Pleerock, quindi devo creare una risposta. Mi dispiace per questo, ma ha fatto un buon lavoro e vorrei rispondergli.

Pleerock, hai creato l'esempio perfetto per mostrare perché quelle costanti dovrebbero essere indipendenti dalle interfacce e indipendenti dall'ereditarietà. Per il cliente dell'applicazione non è importante che ci sia una differenza tecnica tra quelle implementazioni delle auto. Sono gli stessi per il cliente, solo per le auto. Quindi, il cliente vuole guardarli da quella prospettiva, che è un'interfaccia come I_Somecar. In tutta l'applicazione il cliente utilizzerà una sola prospettiva e non differenti per ogni differente marca di auto.

Se un cliente desidera confrontare le auto prima dell'acquisto, può utilizzare un metodo come questo:

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

Un'interfaccia è un contratto sul comportamento e mostra diversi oggetti da una prospettiva. Il modo in cui lo progettate, ogni marchio automobilistico avrà la sua eredità. Anche se in realtà è abbastanza corretto, poiché le auto possono essere così diverse da poter essere come confrontare tipi di oggetti completamente diversi, alla fine c'è la scelta tra auto diverse. E questa è la prospettiva dell'interfaccia che tutti i marchi devono condividere. La scelta delle costanti non dovrebbe renderlo impossibile. Per favore, considera la risposta di Zarkonnen.


-1

Questo deriva da un tempo prima che Java 1.5 esistesse e ci portasse gli enum. In precedenza, non esisteva un modo valido per definire un insieme di costanti o valori vincolati.

Questo è ancora utilizzato, la maggior parte delle volte o per compatibilità con le versioni precedenti o per la quantità di refactoring necessaria per sbarazzarsi, in molti progetti.


2
Prima di Java 5, era possibile utilizzare il modello enum indipendente dai tipi (vedere java.sun.com/developer/Books/shiftintojava/page1.html ).
Dan Dyer
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.