Interfejsy z polami statycznymi w Javie do udostępniania „stałych”


116

Przyglądam się niektórym projektom Java o otwartym kodzie źródłowym, aby dostać się do Javy i zauważam, że wiele z nich ma interfejs „stałych”.

Na przykład processing.org ma interfejs o nazwie PConstants.java , a większość innych klas podstawowych implementuje ten interfejs. Interfejs jest usiany statycznymi elementami. Czy istnieje powód takiego podejścia, czy jest to uważane za złą praktykę? Dlaczego nie użyć wyliczeń, jeśli ma to sens , lub klasy statycznej?

Uważam, że dziwne jest używanie interfejsu, który pozwala na pseudo „zmienne globalne”.

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
Uwaga: static finalnie jest konieczne, dla interfejsu jest nadmiarowe.
ThomasW,

Należy również pamiętać, że platformNamesmoże być public, statici final, ale to na pewno nie jest stała. Jedyną stałą tablicą jest tablica o zerowej długości.
Vlasec

@ThomasW Wiem, że to ma kilka lat, ale musiałem wskazać błąd w Twoim komentarzu. static finalniekoniecznie jest zbędne. Pole klasy lub interfejsu zawierające tylko finalsłowo kluczowe utworzyłoby oddzielne wystąpienia tego pola podczas tworzenia obiektów klasy lub interfejsu. Użycie static finalspowodowałoby, że każdy obiekt współdzieliłby lokalizację pamięci dla tego pola. Innymi słowy, gdyby klasa MyClass miała pole final String str = "Hello";, dla N wystąpień MyClass w pamięci byłoby N wystąpień pola str. Dodanie staticsłowa kluczowego spowodowałoby tylko 1 wystąpienie.
Sintrias

Odpowiedzi:


160

Jest to ogólnie uważane za złą praktykę. Problem polega na tym, że stałe są częścią publicznego "interfejsu" (z braku lepszego słowa) klasy implementującej. Oznacza to, że klasa implementująca publikuje wszystkie te wartości w klasach zewnętrznych, nawet jeśli są one wymagane tylko wewnętrznie. Stałe mnożą się w całym kodzie. Przykładem jest interfejs SwingConstants w Swing, który jest implementowany przez dziesiątki klas, które wszystkie „reeksportują” wszystkie swoje stałe (nawet te, których nie używają) jako własne.

Ale nie wierz mi na słowo, Josh Bloch również mówi, że to źle:

Stały wzorzec interfejsu to słabe wykorzystanie interfejsów. To, że klasa używa wewnętrznie pewnych stałych, jest szczegółem implementacji. Implementacja stałego interfejsu powoduje wyciek szczegółów implementacji do wyeksportowanego interfejsu API klasy. Dla użytkowników klasy nie ma żadnego znaczenia, że ​​klasa implementuje stały interfejs. W rzeczywistości może ich to nawet zmylić. Co gorsza, stanowi zobowiązanie: jeśli w przyszłej wersji klasa zostanie zmodyfikowana tak, aby nie musiała już używać stałych, nadal musi zaimplementować interfejs, aby zapewnić zgodność binarną. Jeśli klasa nie ostateczna implementuje stały interfejs, wszystkie jej podklasy będą miały swoje przestrzenie nazw zanieczyszczone stałymi w interfejsie.

Wyliczenie może być lepszym podejściem. Lub możesz po prostu umieścić stałe jako publiczne pola statyczne w klasie, której nie można utworzyć instancji. Umożliwia to innej klasie dostęp do nich bez zanieczyszczania własnego interfejsu API.


8
Enum to tu czerwony śledź - a przynajmniej osobne pytanie. Oczywiście należy używać wyliczeń, ale powinny one być również ukryte, jeśli nie są potrzebne realizatorom.
DJClayworth

12
BTW: Możesz użyć wyliczenia bez instancji jako klasy, której nie można utworzyć wystąpienia. ;)
Peter Lawrey

5
Ale po co wdrażać te interfejsy w pierwszej kolejności? Dlaczego nie używać ich jako repozytorium stałych? Jeśli potrzebuję jakichś stałych udostępnianych na całym świecie, nie widzę „czystszych” sposobów na zrobienie tego.
shadox

2
@DanDyer tak, ale interfejs sprawia, że ​​niektóre deklaracje są niejawne. Podobnie jak publiczny statyczny finał jest po prostu domyślny. Po co zawracać sobie głowę klasą? Enum - cóż, to zależy. Wyliczenie powinno definiować zbiór możliwych wartości dla jednostki, a nie zbiór wartości dla różnych jednostek.
shadox,

4
Osobiście uważam, że Josh źle uderza piłkę. Jeśli nie chcesz, aby Twoje stałe przeciekały - niezależnie od typu obiektu, który je umieścisz - musisz się upewnić, że NIE są one częścią eksportowanego kodu. Interfejs lub klasa, obie mogą być eksportowane. Zatem właściwe pytanie nie brzmi: w jakim typie obiektów je umieszczam, ale jak organizuję ten obiekt. A jeśli stałe są używane w wyeksportowanym kodzie, nadal należy się upewnić, że są one dostępne po wyeksportowaniu. Zatem twierdzenie o „złej praktyce” jest moim skromnym zdaniem nieważne.
Lawrence

99

Zamiast implementować „interfejs stałych”, w Javie 1.5+ możesz użyć importu statycznego, aby zaimportować stałe / metody statyczne z innej klasy / interfejsu:

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

Pozwala to uniknąć brzydoty polegającej na tym, że klasy implementują interfejsy, które nie mają żadnej funkcjonalności.

Jeśli chodzi o praktykę posiadania klasy tylko do przechowywania stałych, myślę, że czasami jest to konieczne. Istnieją pewne stałe, które po prostu nie mają naturalnego miejsca w klasie, więc lepiej mieć je w „neutralnym” miejscu.

Ale zamiast używać interfejsu, użyj końcowej klasy z prywatnym konstruktorem. (Uniemożliwiając utworzenie instancji lub podklasy klasy, wysyłając silny komunikat, że nie zawiera ona niestatycznej funkcjonalności / danych.)

Na przykład:

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

Więc wyjaśniasz, że z powodu statycznego importu powinniśmy użyć klas zamiast interfejsów, aby powtórzyć ten sam błąd co wcześniej ?! To głupie!
gizmo

11
Nie, wcale tego nie mówię. Mówię dwie niezależne rzeczy. 1: używaj importu statycznego zamiast nadużywania dziedziczenia. 2: Jeśli musisz mieć repozytorium stałych, uczyń z niego ostatnią klasę zamiast interfejsu.
Zarkonnen

„Stałe interfejsy”, jeśli nie są przeznaczone do dziedziczenia, nigdy. Więc statyczny import dotyczy tylko cukru składniowego, a dziedziczenie z takiego interfejsu jest okropnym błędem. Wiem, że Sun to zrobił, ale popełnili też mnóstwo innych podstawowych błędów, to nie jest wymówka, żeby ich naśladować.
gizmo

3
Jednym z problemów z kodem opublikowanym w pytaniu jest to, że implementacja interfejsu jest używana tylko po to, aby uzyskać łatwiejszy dostęp do stałych. Kiedy widzę coś implementującego FooInterface, spodziewam się, że wpłynie to na jego funkcjonalność, a powyższe narusza to. Import statyczny rozwiązuje ten problem.
Zarkonnen

2
gizmo - nie jestem fanem importu statycznego, ale on tam robi, aby uniknąć konieczności używania nazwy klasy, czyli ConstClass.SOME_CONST. Wykonanie importu statycznego nie dodaje tych elementów do klasy, której Z. nie mówi, że ma dziedziczyć po interfejsie, mówi wręcz przeciwnie.
mtruesdell

8

Nie udaję, że mam rację, ale zobaczmy ten mały przykład:

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 nie wie nic o FordCar, a FordCar nie wie nic o ToyotaCar. zasada CarConstants powinna zostać zmieniona, ale ...

Stałych nie należy zmieniać, ponieważ koło jest okrągłe, a silnik jest mechaniczny, ale ... W przyszłości inżynierowie Toyoty wynaleźli elektroniczny silnik i płaskie koła! Zobaczmy nasz nowy interfejs

public interface InnovativeCarConstants {

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

a teraz możemy zmienić naszą abstrakcję:

public interface ToyotaCar extends CarConstants

do

public interface ToyotaCar extends InnovativeCarConstants 

A teraz, jeśli kiedykolwiek będziemy musieli zmienić podstawową wartość, jeśli SILNIK lub KOŁO, możemy zmienić interfejs ToyotaCar na poziomie abstrakcji, nie dotykając implementacji

Wiem, że to NIEBEZPIECZNE, ale nadal chcę wiedzieć, czy o tym myślisz


Chcę poznać Twój pomysł już teraz w 2019 roku. Dla mnie pola interfejsu są przeznaczone do współdzielenia między niektórymi obiektami.
Deszcz

Napisałem odpowiedź związaną z Twoim pomysłem: stackoverflow.com/a/55877115/5290519
Raining

jest to dobry przykład tego, jak bardzo zasady PMD są bardzo ograniczone. Próba uzyskania lepszego kodu poprzez biurokrację pozostaje daremna.
bebbo

6

W Javie jest dużo nienawiści do tego wzorca. Jednak interfejs stałych statycznych ma czasami wartość. Zasadniczo musisz spełnić następujące warunki:

  1. Pojęcia są częścią publicznego interfejsu kilku klas.

  2. Ich wartości mogą ulec zmianie w przyszłych wersjach.

  3. Bardzo ważne jest, aby wszystkie implementacje używały tych samych wartości.

Na przykład załóżmy, że piszesz rozszerzenie do hipotetycznego języka zapytań. W tym rozszerzeniu zamierzasz rozszerzyć składnię języka o kilka nowych operacji, które są obsługiwane przez indeks. Np. Będziesz mieć R-Tree obsługujące zapytania geoprzestrzenne.

Więc piszesz publiczny interfejs ze stałą statyczną:

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

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

Później nowy programista myśli, że musi zbudować lepszy indeks, więc przychodzi i buduje implementację R *. Wdrażając ten interfejs w swoim nowym drzewie, gwarantuje, że różne indeksy będą miały identyczną składnię w języku zapytań. Co więcej, jeśli później zdecydujesz, że „nearTo” to myląca nazwa, możesz zmienić ją na „withinDistanceInKm” i wiedzieć, że nowa składnia będzie przestrzegana we wszystkich implementacjach indeksu.

PS: Inspiracją dla tego przykładu jest kod przestrzenny Neo4j.


5

Z perspektywy czasu widzimy, że Java jest zepsuta na wiele sposobów. Jedną z głównych wad Javy jest ograniczenie interfejsów do metod abstrakcyjnych i statycznych pól końcowych. Nowsze, bardziej wyrafinowane języki OO, takie jak Scala, podciągają interfejsy według cech, które mogą (i zazwyczaj tak robią) obejmują konkretne metody, które mogą mieć zerową wartość liczbową (stałe!). Aby zapoznać się z wykładem na temat cech jako jednostek zachowań komponowalnych, zobacz http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Krótki opis porównania cech w Scali z interfejsami w Javie znajduje się pod adresem http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. W kontekście nauczania projektowania obiektów obiektowych, uproszczone zasady, takie jak twierdzenie, że interfejsy nigdy nie powinny zawierać pól statycznych, są głupie. Wiele cech naturalnie obejmuje stałe i te stałe są odpowiednio częścią publicznego „interfejsu” obsługiwanego przez cechę. Podczas pisania kodu Java nie ma przejrzystego, eleganckiego sposobu przedstawiania cech, ale używanie statycznych pól końcowych w interfejsach jest często częścią dobrego obejścia.


12
Strasznie pretensjonalne i przestarzałe.
Esko

1
Wspaniały wgląd (+1), choć może trochę zbyt krytyczny w stosunku do Javy.
peterh - Przywróć Monikę

0

Zgodnie ze specyfikacją JVM, pola i metody w interfejsie mogą mieć tylko Public, Static, Final i Abstract. Odniesienie do Inside Java VM

Domyślnie wszystkie metody w interfejsie są abstrakcyjne, nawet jeśli nie wspomniałeś o tym bezpośrednio.

Interfejsy służą jedynie do podawania specyfikacji. Nie może zawierać żadnych implementacji. Aby uniknąć implementowania klas w celu zmiany specyfikacji, jest to ostateczne. Ponieważ nie można utworzyć wystąpienia interfejsu, są one ustawiane jako statyczne, aby uzyskać dostęp do pola przy użyciu nazwy interfejsu.


0

Nie mam wystarczającej reputacji, aby dać komentarz Pleerockowi, dlatego muszę stworzyć odpowiedź. Przepraszam za to, ale włożył w to sporo wysiłku i chciałbym mu odpowiedzieć.

Pleerock, stworzyłeś doskonały przykład, aby pokazać, dlaczego te stałe powinny być niezależne od interfejsów i niezależne od dziedziczenia. Dla klienta aplikacji nie jest istotne, że istnieje różnica techniczna pomiędzy wdrożeniami samochodów. Dla klienta są takie same, tylko samochody. Tak więc klient chce spojrzeć na nie z tej perspektywy, która jest interfejsem takim jak I_Somecar. W całej aplikacji klient będzie używał tylko jednej perspektywy, a nie różnych dla każdej marki samochodu.

Jeśli klient chce porównać samochody przed zakupem, może skorzystać z takiej metody:

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

Interfejs jest umową dotyczącą zachowania i przedstawia różne obiekty z jednej perspektywy. Sposób, w jaki go zaprojektujesz, sprawi, że każda marka samochodów będzie miała własną linię dziedziczenia. Chociaż w rzeczywistości jest to całkiem poprawne, ponieważ samochody mogą być tak różne, że można porównać zupełnie różne typy obiektów, w końcu istnieje wybór między różnymi samochodami. I to jest perspektywa interfejsu, którą muszą dzielić wszystkie marki. Wybór stałych nie powinien tego uniemożliwiać. Proszę, rozważ odpowiedź Zarkonnena.


-1

To pochodzi z czasów, zanim istniała Java 1.5 i udostępniła nam wyliczenia. Wcześniej nie było dobrego sposobu definiowania zestawu stałych lub ograniczonych wartości.

Jest to nadal używane, przez większość czasu albo ze względu na kompatybilność wsteczną, albo ze względu na ilość refaktoryzacji potrzebnej do pozbycia się, w wielu projektach.


2
W wersjach starszych niż Java 5 można było używać wzorca wyliczenia bezpiecznego dla typów (patrz java.sun.com/developer/Books/shiftintojava/page1.html ).
Dan Dyer
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.