Інтерфейси зі статичними полями в java для обміну "константами"


116

Я дивлюся на якісь проекти Java з відкритим кодом, щоб потрапити на Java і зауважую, що багато з них мають якийсь інтерфейс "константи".

Наприклад, processing.org має інтерфейс під назвою PConstants.java , і більшість інших основних класів реалізують цей інтерфейс. Інтерфейс пронизаний статичними членами. Чи є причина такого підходу, чи це вважається поганою практикою? Чому б не використовувати enums там, де це має сенс , або статичний клас?

Мені здається дивним використовувати інтерфейс, щоб передбачити якусь псевдо «глобальну змінну».

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
Примітка: static finalне потрібно, це зайве для інтерфейсу.
ThomasW

Також зверніть увагу , що platformNamesможе бути public, staticі final, але це, безумовно , не є постійною. Єдиний постійний масив - це нульова довжина.
Власек

@ThomasW Я знаю, що це вже кілька років, але мені потрібно було вказати на помилку у вашому коментарі. static finalне обов'язково є зайвим. Поле класу або інтерфейсу лише з finalключовим словом створює окремі екземпляри цього поля під час створення об’єктів класу чи інтерфейсу. Використання static finalзмусило б кожен об'єкт розділити пам'ять для цього поля. Іншими словами, якби клас MyClass мав поле final String str = "Hello";, для N екземплярів MyClass було б N екземплярів str поля. Додавання staticключового слова призведе до лише 1 примірника.
Сінтрій

Відповіді:


160

Зазвичай це вважається поганою практикою. Проблема полягає в тому, що константи є частиною загальнодоступного "інтерфейсу" (на користь кращого слова) класу реалізації. Це означає, що клас-реалізатор публікує всі ці значення для зовнішніх класів, навіть коли вони вимагаються лише внутрішньо. Константи поширюються протягом коду. Прикладом може бути інтерфейс SwingConstants в Swing, який реалізується десятками класів, які всі "реекспортують" усі його константи (навіть ті, які вони не використовують) як свої.

Але не прийміть на це слово, Джош Блох також каже, що це погано:

Постійна схема інтерфейсу - це погане використання інтерфейсів. Те, що клас використовує деякі константи внутрішньо, є деталлю реалізації. Реалізація постійного інтерфейсу змушує цю деталь реалізації просочитися до експортованого API класу. Для користувачів класу не має жодних наслідків, що клас реалізує постійний інтерфейс. Насправді це може навіть збити з пантелику. Гірше, що це означає зобов'язання: якщо в майбутньому випуску клас буде змінено так, що йому більше не потрібно використовувати константи, він все одно повинен реалізувати інтерфейс для забезпечення бінарної сумісності. Якщо нефінальний клас реалізує постійний інтерфейс, усі його підкласи матимуть свої простори імен, забруднені константами в інтерфейсі.

Перевага може бути кращим підходом. Або ви можете просто поставити константи як публічні статичні поля в класі, який неможливо встановити. Це дозволяє іншому класу отримати доступ до них без забруднення власного API.


8
Енуми - це червона оселедець - або, принаймні, окреме питання. Звичайно, слід використовувати енуми, але їх також слід приховати, якщо вони не потрібні виконавцям.
DJClayworth

12
BTW: Ви можете використовувати enum без жодних екземплярів як клас, який неможливо встановити. ;)
Пітер Лорі

5
Але навіщо реалізовувати ці інтерфейси в першу чергу? Чому б не використовувати їх просто як сховище констант? Якщо мені потрібні якісь константи, що діляться в усьому світі, я не бачу "чистіших" способів це зробити.
shadox

2
@DanDyer Так, але інтерфейс робить деякі декларації неявними. Як і загальнодоступний статичний фінал - це лише за замовчуванням. Навіщо турбуватися з класом? Енум - ну, це залежить. Перерахунок повинен визначати сукупність можливих значень для сутності, а не набір значень для різних сутностей.
shadox

4
Особисто я відчуваю, що Джош неправильно б'є по м'ячу. Якщо ви не хочете, щоб ваші константи просочились - незалежно від того, в який тип об'єкта ви їх помістили - вам потрібно переконатися, що вони НЕ є частиною експортованого коду. Інтерфейс або клас можуть бути експортовані. Тож правильне запитання не в тому: у який тип об’єктів я їх розміщую, а як я впорядковую цей об’єкт. І якщо константи використовуються в експортованому коді, ви все ж переконайтеся, що вони доступні після експорту. Тож вимога "поганої практики" на мою скромну думку є недійсною.
Лоуренс

99

Замість того, щоб реалізувати "константний інтерфейс", у Java 1.5+ ви можете використовувати статичний імпорт для імпорту констант / статичних методів з іншого класу / інтерфейсу:

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

Це дозволяє уникнути неподобства змушувати ваші класи реалізовувати інтерфейси, які не мають функціоналу.

Що стосується практики проведення класу просто для зберігання констант, я думаю, що іноді це потрібно. Є певні константи, які просто не мають природного місця в класі, тому краще розмістити їх у "нейтральному" місці.

Але замість інтерфейсу використовуйте заключний клас із приватним конструктором. (Унеможливлюючи інстанціювання або підклас класу, надсилаючи сильне повідомлення про те, що він не містить нестатичних функціональних можливостей / даних.)

Наприклад:

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

Отже, ви пояснюєте, що через статичний імпорт нам слід використовувати класи замість інтерфейсів, щоб повторити таку ж помилку, що і раніше ?! Це нерозумно!
gizmo

11
Ні, це зовсім не те, що я говорю. Я кажу дві незалежні речі. 1: використовувати статичний імпорт замість зловживання спадщиною. 2: Якщо у вас є сховище констант, зробіть його остаточним класом замість інтерфейсу.
Зарконнен

"Постійні інтерфейси", де не призначені для того, щоб бути частиною будь-якої спадщини, ніколи. Тож статичний імпорт призначений саме для синтаксичного цукру, а успадковане з такого інтерфейсу жахлива помилка. Я знаю, що Сонце це робило, але вони також зробили багато інших основних помилок, це не привід імітувати їх.
gizmo

3
Однією з проблем з кодом, розміщеним у питанні, є те, що реалізація інтерфейсу використовується лише для того, щоб полегшити доступ до констант. Коли я бачу щось реалізувати FooInterface, я очікую, що це вплине на його функціональність, і вищесказане порушує це. Статичний імпорт вирішує цю проблему.
Зарконен

2
gizmo - Я не прихильник статичного імпорту, але те, що він там робить, уникає використання назви класу, тобто ConstClass.SOME_CONST. Здійснення статичного імпорту не додає цих членів до класу, якому ви не скажете наслідувати інтерфейс, він каже, що навпаки.
mtruesdell

8

Я не претендую на право, але маю на увазі цей невеликий приклад:

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 нічого не знає про FordCar, а FordCar не знає про ToyotaCar. Принцип CarConstants слід змінити, але ...

Константи не слід міняти, тому що колесо кругле, а egine механічне, але ... Надалі інженери-дослідники Toyota винайшли електронні двигуни та плоскі колеса! Давайте подивимось наш новий інтерфейс

public interface InnovativeCarConstants {

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

і тепер ми можемо змінити нашу абстракцію:

public interface ToyotaCar extends CarConstants

до

public interface ToyotaCar extends InnovativeCarConstants 

І тепер, якщо нам колись потрібно змінити основне значення, якщо ДВИГАТЕЛЬ або КОЛИЦЕ, ми можемо змінити інтерфейс ToyotaCar на рівні абстракції, не торкаючись реалізацій

Я НЕ БЕЗПЕЧНИЙ, я знаю, але я все одно хочу знати, що ви думаєте з цього приводу


Я хочу знати, що вам ідея зараз, у 2019 році. Для мене поля інтерфейсу мають бути розділені між деякими об'єктами.
Дощ

Я написав відповідь, пов’язану з вашою ідеєю: stackoverflow.com/a/55877115/5290519
Дощ

це хороший приклад того, як правила PMD дуже обмежені. Спроба отримати кращий код через бюрократію залишається марною спробою.
bebbo

6

На Яві дуже багато ненависті для цього шаблону. Однак інтерфейс статичних констант іноді має значення. Потрібно в основному виконати такі умови:

  1. Концепції є частиною публічного інтерфейсу декількох класів.

  2. Їх значення можуть змінитися в майбутніх випусках.

  3. Важливо, що всі реалізації використовують однакові значення.

Наприклад, припустимо, що ви пишете розширення до гіпотетичної мови запитів. У цьому розширенні ви збираєтеся розширити синтаксис мови деякими новими операціями, які підтримуються індексом. Наприклад, у вас буде R-дерево, що підтримує геопросторові запити.

Отже, ви пишете загальнодоступний інтерфейс зі статичною постійною:

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

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

Тепер пізніше новий розробник думає, що йому потрібно створити кращий індекс, тому він приходить і будує реалізацію R *. Реалізуючи цей інтерфейс у своєму новому дереві, він гарантує, що різні індекси матимуть однаковий синтаксис на мові запитів. Крім того, якщо пізніше ви вирішили, що "nearTo" є заплутаною назвою, ви можете змінити його на "withinDistanceInKm" і знати, що новий синтаксис буде дотриманий усіма вашими реалізаціями індексу.

PS: Натхнення для цього прикладу беремо з просторового коду Neo4j.


5

Враховуючи перевагу заднього огляду, ми можемо побачити, що Java багато в чому зламана. Однією з основних помилок Java є обмеження інтерфейсів абстрактними методами та статичними кінцевими полями. Новіші, більш досконалі мови OO, такі як Scala, містять інтерфейси за ознаками, які можуть (і, як правило, включають) конкретні методи, які можуть мати нульову аритність (константи!). Експозицію щодо ознак як одиниць композиційної поведінки див . У розділі http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Короткий опис того, як риси Scala порівнюються з інтерфейсами на Java, див. Http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. У контексті навчання дизайну OO спрощені правила, такі як твердження, що інтерфейси ніколи не повинні містити статичних полів, нерозумні. Звичайно, багато ознак включають константи, і ці константи належним чином є частиною загальнодоступного "інтерфейсу", який підтримується ознакою. У написанні Java-коду немає чіткого, елегантного способу представлення ознак, але використання статичних кінцевих полів в інтерфейсах часто є частиною хорошого вирішення.


12
Страшенно претензійний і нині застарілий.
Есько

1
Чудове розуміння (+1), хоча, можливо, трохи надто критично ставиться до Java.
peterh

0

Відповідно до специфікації JVM, поля та методи в інтерфейсі можуть мати лише загальнодоступні, статичні, заключні та абстрактні. Посилання від Inside Java VM

За замовчуванням всі методи в інтерфейсі є абстрактними, навіть якщо ви прямо не згадали про це.

Інтерфейси покликані давати лише специфікації. Він не може містити жодних реалізацій. Щоб уникнути впровадження класів для зміни специфікації, вона стає остаточною. Оскільки інтерфейс неможливо встановити, вони робляться статичними для доступу до поля за допомогою імені інтерфейсу.


0

Мені не вистачає репутації, щоб дати коментар Pleerock, для цього мені потрібно створити відповідь. Мені шкода за це, але він приклав до цього трохи зусиль, і я хотів би відповісти на нього.

Плерок, ви створили ідеальний приклад, щоб показати, чому ці константи повинні бути незалежними від інтерфейсів і незалежними від успадкування. Для клієнта програми не важливо, що існує технічна різниця між реалізацією автомобілів. Вони однакові для клієнта, просто машини. Отже, клієнт хоче подивитися на них з тієї точки зору, який є інтерфейсом, як I_Somecar. Протягом усієї програми клієнт використовуватиме лише одну перспективу та не різні для кожної марки автомобілів.

Якщо клієнт хоче порівняти машини перед покупкою, у нього може бути такий метод:

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

Інтерфейс - це договір про поведінку і показує різні об'єкти з однієї точки зору. У тому, як ви його проектуєте, чи буде кожна марка автомобіля власною лінією спадкування. Хоча насправді це цілком правильно, адже автомобілі можуть бути настільки різними, що можуть бути схожими на порівняння абсолютно різних типів предметів, врешті-решт є вибір між різними автомобілями. І в цьому полягає перспектива інтерфейсу, який повинні використовувати всі бренди. Вибір констант не повинен робити це неможливим. Будь ласка, врахуйте відповідь Зарконнена.


-1

Це з'явилося за часів існування Java 1.5 і принесло нам перерахунки. До цього не було хорошого способу визначення набору констант або обмежених значень.

Це все ще використовується в більшості випадків або для зворотної сумісності, або через кількість рефакторингу, необхідного для позбавлення, у багатьох проектах.


2
До Java 5 ви можете використовувати безпечний для стилю шаблон (див. Java.sun.com/developer/Books/shiftintojava/page1.html ).
Ден Дайер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.