Но можно ли вообще сделать перечисления динамическими?
Перечисления, по определению, являются статическими. Они используются для “предопределения” констант. Я позволю официальному Oracle Java Tutorial по перечислениям помочь объяснить определение.
Тип перечисления – это специальный тип данных, который позволяет переменной представлять собой набор предопределенных констант. Переменная должна быть равна одному из значений, которые были для нее предопределены. Общие примеры включают направления по компасу (значения СЕВЕРА, ЮГА, ВОСТОКА и ЗАПАДА) и дни недели.
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY
}
Значения перечисления известны компилятору во время компиляции и не должны изменяться. Представьте, что кто-то попытался бы ввести новый день недели. Это довольно маловероятно, и, таким образом, во всех отношениях дни недели остаются прежними.
Однако сейчас вы работаете над приложением, которое имеет устаревшую кодовую базу. Этот устаревший код имеет перечисление с именем Color ( британская орфография, потому что я не в США/| ). Фактическое перечисление может быть чем-то другим и сложным, но пример Color подходит для целей этой статьи.
Цвет имеет три цвета, определенные внутри него – КРАСНЫЙ , ЗЕЛЕНЫЙ и СИНИЙ .
public enum Colour {
RED(255, 0, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255);
int r, g, b;
Colour(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
Перечисление Color используется во многих приложениях в качестве зависимости. Во-первых, вы задаетесь вопросом, зачем кому-то понадобилось указывать Цвета в качестве перечисляемого типа данных. Но вы ничего не можете с этим поделать, вы просто принимаете это и работаете с тем, что уготовано вам судьбой.
Проблема с приведенным выше перечислением заключается в том, что каждый раз, когда какому-либо приложению требуется новый цвет, вам нужно добавить новый цвет в свою кодовую базу. После добавления нового цвета вам теперь нужно протестировать, а затем повторно развернуть все ваши приложения. Это необходимо делать каждый раз, когда запрашивается новый цвет. Вы видите проблему – цвета должны быть динамичными, но кто-то принял решение вечность назад, и теперь вы один в супе.
Теперь вы хотите изменить перечисления, чтобы сделать их динамическими, чтобы любой добавленный новый цвет извлекался из базы данных с минимальными изменениями кода в вашем стеке. Как вы поступаете дальше?
Перечисления также являются классами
Да! Перечисления также являются классами. Мы знаем, что перечисления являются статическими, а классы – нет. Технически вы могли бы заменить Enum классом. Если вы замените перечисление классом, вам даже не нужно будет изменять импорт в других классах. Ни одна часть вашей кодовой базы даже не осознает этого! Это будет основой нашего решения – превратить Enum в класс.
Но подождите! Это не так просто. На перечисления также можно ссылаться напрямую, например Color. КРАСНЫЙ . Мы не хотим вмешиваться в существующие способы использования Color. КРАСНЫЙ в нашем коде.
Шаг 1: Измените перечисление на класс
Когда вы меняете enum на class , ваша IDE должна немедленно выдать ошибку. Это связано с тем, что перечисления – это конструкции времени компиляции, на которые ссылаются в других частях вашего приложения. Но не волнуйтесь!
Шаг 2: Создание констант существующих цветов
Чтобы сохранить обычаи Color. RED то же самое, нам нужно было бы создать константы для всех цветов, определенных в enum (теперь класс).
public final class Colour {
public static final Colour RED = new Colour(255, 0, 0);
public static final Colour GREEN = new Colour(0, 255, 0);
public static final Colour BLUE = new Colour(0, 0, 255);
private final int r, g, b;
private Colour(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
Мы также не хотим, чтобы объекты Цвет должен быть создан вне класса. Если мы этого не сделаем, мы, по сути, позволим любому создавать объекты Color и лишим цели имитировать перечисление. Таким образом, мы бы пометили конструктор как private .
Поздравляю! Теперь IDE должна перестать выдавать ошибку (по крайней мере, внутри класса Color ). Приведенный выше рефакторинг гарантирует, что вы все еще можете ссылаться на Color. КРАСНЫЙ как и ранее. Кроме того, теперь, когда мы сделали переменные r , g и b как private final , поскольку мы не хотим, чтобы они менялись после создания объекта.
Шаг 3: Реализуйте другие методы перечисления, такие как values() и valueOf()
Мы знаем, что такие методы, как values() и valueOf() довольно часто используются с перечислениями. Чтобы гарантировать, что эти обычаи не нарушаются, нам нужно было бы “имитировать” эти методы. Как мы можем это сделать?
Обратите внимание на возвращаемые типы values() и значение() .
valueOf(String)возвращает экземплярColor, определенный именем, указанным в качестве параметра.values()возвращает массивЦвет, т.е.Цвет[]
Давайте начнем с метода value Of() , и это приведет нас к решению values()
Как мы знаем, значение() принимает Строка в качестве аргумента и возвращает экземпляр |/Color . Что мы можем использовать, чтобы сохранить сопоставление между String и Цвет ? Карта!! Мы можем использовать хэш-карту/| или ConcurrentHashMap для этого.
Обновление 17-07-2021: НЕ ИСПОЛЬЗУЙТЕ Хэш-карта или ConcurrentHashMap , вместо этого используйте LinkedHashMap , по причинам, которые будут объяснены позже в статье.
Обратите внимание, что в настоящее время у нас нет возможности узнать, какого цвета экземпляр. Конечно, у нас есть константа, объявленная как
public static final Colour RED = new Colour(255, 0, 0);
но даже если мы загрузим его на карту, как мы узнаем, предназначен ли объект для Color. КРАСНЫЙ или Цвет. ЗЕЛЕНЫЙ ? Есть ли способ, которым мы можем получить “текст”/”имя” переменной в виде Строка ? Не напрямую, нет.
Введите: Отражение Java
Это позволяет исполняемой Java-программе исследовать или “анализировать” саму себя и манипулировать внутренними свойствами программы. Например, класс Java может получать имена всех своих членов и отображать их.
Мы можем использовать это! Давайте сначала определим карту типа Карта<Строка, цвет> в виде хэш-карты или ConcurrentHashMap . Затем мы будем использовать Java Reflection для загрузки значений внутри карты через static блок.
public final class Colour {
public static final Colour RED = new Colour(255, 0, 0);
public static final Colour GREEN = new Colour(0, 255, 0);
public static final Colour BLUE = new Colour(0, 0, 255);
private static final Map map = new LinkedHashMap<>();
static {
loadClassData();
}
private static void loadClassData() {
Arrays.stream(Colour.class.getDeclaredFields())
.filter(declaredField -> declaredField.getType() == Colour.class)
.forEach(Colour::putInMap);
}
private static void putInMap(Field declaredField) {
try {
map.putIfAbsent(declaredField.getName(), (Colour) declaredField.get(null));
} catch (IllegalAccessException e) {
System.err.println("Could not initialize Colour Map value: " + declaredField.getName() + " " + e);
}
}
private final int r, g, b;
private Colour(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
Приведенный выше блок кода использует потоки Java для загрузки карты. Давайте рассмотрим, что он делает.
- Функция
Color.class.getDeclaredFields()извлекает все поля, объявленные внутри классаColor. - Для каждого возвращаемого поля нам нужны только поля типа
Цвет. (Предыдущий оператор также вернул быMap<Строка, цвет>) - Для каждого из полей
Цвет,, мы бы вызвали методputInMap() - .
Функцияput In Map()принимает параметр типаFieldи загружает данные на карту. Имя переменной получается с помощьюdeclared Field.getName(), а фактический объект возвращается с помощью
Если вы не понимаете приведенный выше код на основе потока или просто работаете с Java 7 и ниже, вы можете использовать следующее:
private static void loadClassData() {
for (Field declaredField : Colour.class.getDeclaredFields()) {
if (declaredField.getType() == Colour.class) {
putInMap(declaredField);
}
}
}
У нас есть данные на карте! Теперь мы можем просто реализовать метод valueOf() следующим образом:
public static Colour valueOf(String name) {
Colour colour = map.get(name);
if (colour == null) {
throw new IllegalArgumentException("No Colour by the name " + name + " found");
}
return colour;
}
Обратите внимание, что в Enums valueOf() возвращает исключение IllegalArgumentException если в перечислении не найдено никакого значения. Аналогичным образом, мы позаботимся о том, чтобы наша реализация также возвращала то же исключение.
Использование Map позволяет нам легко реализовать метод values()/|. Мы можем реализовать это с помощью метода map.values() .
public static Colour[] values() {
return map.values().toArray(Colour[]::new).clone();
}
Обновление 14-07-2021: Карта не имеет упорядоченных значений, поэтому нам нужно ее отсортировать! Тесты были обновлены в последующих разделах.
Обновление 17-07-2021: С момента значения() метод создает массив в том порядке, в котором определены значения Enum, нам также необходимо сохранить порядок на карте. Вот почему нам нужно использовать LinkedHashMap , а не другая реализация карты.
Каждый раз, когда вызывается values() , он возвращает клон значений массива на карте.
Шаг 4: Загрузите данные из базы данных
Однако цель наличия динамического перечисления все еще не достигнута. Мы хотим, чтобы значения цвета загружались из базы данных. Теперь проблема кажется тривиальной. Подобно тому, как мы загрузили данные класса внутри карты, мы также должны извлечь данные из базы данных и загрузить их. Это также должно происходить в статическом блоке.
Однако в этом случае мы не можем использовать отражение Java для получения имени переменной просто потому, что нет никакой статической переменной, на которую мы могли бы ссылаться.
Таким образом, мы должны добавить еще одно поле в класс с именем Название цвета .
public final class Colour {
public static final Colour RED = new Colour("RED", 255, 0, 0);
public static final Colour GREEN = new Colour("GREEN", 0, 255, 0);
public static final Colour BLUE = new Colour("BLUE", 0, 0, 255);
private static final Map map = new LinkedHashMap<>();
// other implemented methods
// new field
private final String colourName;
private final int r, g, b;
private Colour(String colourName, int r, int g, int b) {
this.colourName = colourName;
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
Таким образом, когда мы загружаем данные из базы данных, мы будем знать, какой ключ использовать для карты.
Наш статический блок теперь будет иметь
static {
loadClassData();
loadDataFromDb();
}
private static void loadDataFromDb() {
List colourData = new ColourDB().getColours();
for (ColourDB.ColourData colourDatum : colourData) {
map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum));
}
}
Наша Color DB содержит статический класс Color Data , который точно такой же, как Color POJO. Поскольку мы не можем создавать объекты Color , нам нужен другой тип для размещения данных и получения данных.
Color DB выглядит следующим образом:
public class ColourDB {
public List getColours() {
// data from DB
}
static class ColourData {
String colourName;
int r;
int g;
int b;
public ColourData(String colourName, int r, int g, int b) {
this.colourName = colourName;
this.r = r;
this.g = g;
this.b = b;
}
// getters
}
}
Теперь мы можем добавить еще один частный конструктор в Color , который принимает Данные о цвете .
private Colour(ColourDB.ColourData colourDatum) {
this.colourName = colourDatum.getColourName();
this.r = colourDatum.getR();
this.g = colourDatum.getG();
this.b = colourDatum.getB();
}
Обратите внимание, что использование этого метода позволяет сделать цвета динамическими, но не позволяет создавать статические объекты, такие как Color. КРАСНЫЙ . Если мы добавим данные для цвета “ЧЕРНЫЙ” в базу данных, мы не сможем ссылаться на него как на Color. ЧЕРНЫЙ после этого изменения. Нам нужно было бы сослаться на него как |/Color.value Of(“ЧЕРНЫЙ”) и получить значение. Это компромисс, необходимый для того, чтобы сделать его динамичным. Однако это позволяет нам гарантировать, что на существующий код это не повлияет.
Если у вас есть реализованный метод получения для имени цвета , измените его с getColourName() на name() .
Аналогично, если вы используете ordinal() метод enum, убедитесь, что вы также ввели поле ordinal в классе Color . Вам также нужно будет сохранить порядковое поле в базе данных.
Вы также можете реализовать метод equals() |/и изменить его на сравнение с использованием = = , как это делается в Enums. Также необходимо было бы реализовать интерфейс Comparable с помощью метода compareTo() .
Мы также внедрим сериализуемый интерфейс, который позволит нам сериализовать объекты. Это гарантирует, что мы соответствуем Перечисление функциональность. Кроме того, перечисления не могут быть клонированы. Следовательно, мы также реализуем метод clone и создадим исключение CloneNotSupportedException .
Обновленный код будет выглядеть следующим образом:
public final class Colour implements Comparable, Serializable { public static final Colour RED = new Colour("RED", 255, 0, 0, 0); public static final Colour GREEN = new Colour("GREEN", 0, 255, 0, 1); public static final Colour BLUE = new Colour("BLUE", 0, 0, 255, 2); private static final Map map = new LinkedHashMap<>(); static { loadClassData(); loadDataFromDb(); } private static void loadClassData() { Arrays.stream(Colour.class.getDeclaredFields()) .filter(declaredField -> declaredField.getType() == Colour.class) .forEach(Colour::putInMap); } private static void loadDataFromDb() { List colourData = new ColourDB().getColours(); for (ColourDB.ColourData colourDatum : colourData) { map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum)); } } public static Colour[] values() { return map.values().toArray(Colour[]::new).clone(); } public static Colour valueOf(String name) { Colour colour = map.get(name); if (colour == null) { throw new IllegalArgumentException("No Colour by the name " + name + " found"); } return colour; } private final String colourName; private final int r, g, b; private final int ordinal; private Colour(String colourName, int r, int g, int b, int ordinal) { this.colourName = colourName; this.r = r; this.g = g; this.b = b; this.ordinal = ordinal; } private Colour(ColourDB.ColourData colourData) { this.colourName = colourData.getColourName(); this.r = colourData.getR(); this.g = colourData.getG(); this.b = colourData.getB(); this.ordinal = colourData.getOrdinal(); } private static void putInMap(Field declaredField) { try { map.putIfAbsent(declaredField.getName(), (Colour) declaredField.get(null)); } catch (IllegalAccessException e) { System.err.println("Could not initialize Colour Map value: " + declaredField.getName() + " " + e); } } public String name() { return colourName; } public int ordinal() { return ordinal; } // getters // .. // .. @Override public boolean equals(Object o) { return this == o; } @Override public int hashCode() { return Objects.hash(colourName, r, g, b, ordinal); } @Override public final int compareTo(Colour o) { Colour self = this; return self.ordinal - o.ordinal; } @Override protected Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } @Override public String toString() { return "Colour{" + "colourName='" + colourName + '\'' + ", r=" + r + ", g=" + g + ", b=" + b + ", ordinal=" + ordinal + '}'; } }
и Окрашенный как
public class ColourDB {
public List getColours() {
// data from DB
}
static class ColourData {
String colourName;
int r;
int g;
int b;
int ordinal;
public ColourData(String colourName, int r, int g, int b, int ordinal) {
this.colourName = colourName;
this.r = r;
this.g = g;
this.b = b;
this.ordinal = ordinal;
}
// getters
}
}
Шаг 5: Тест
Обычно это сработало бы. Если, конечно, вы действительно не хотите писать модульные тесты для класса.
Однако как мы можем это проверить? Обратите внимание, что протестировать его практически невозможно, если у вас нет реальной базы данных. Это происходит из-за следующего фрагмента кода.
private static void loadDataFromDb() {
List colourData = new ColourDB().getColours();
for (ColourDB.ColourData colourDatum : colourData) {
map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum));
}
}
Здесь зависимость для базы данных new Color DB() жестко запрограммирована внутри класса. Для тестирования потребуется фактическое подключение к базе данных. Мы хотели бы поиздеваться над ним во время тестирования. Если бы мы использовали DI-фреймворк, такой как Spring, мы могли бы ввести его. Однако использование чистого кода Java потребует дополнительного рефакторинга.
Во-первых, нам нужно извлечь Color DB в интерфейс и включить фактическую реализацию как ColourDbImpl .
public interface ColourDB {
List getColours();
class ColourData {
// existing
}
}
public class ColourDBImpl implements ColourDB {
@Override
public List getColours() {
// get from DB
}
}
Теперь мы создадим класс с именем DB :
public class DB {
private static ColourDB COLOUR_DB;
public DB(ColourDB colourDB) {
COLOUR_DB = colourDB;
}
public static ColourDB getColourDb() {
if (COLOUR_DB == null) {
COLOUR_DB = new ColourDBImpl();
}
return COLOUR_DB;
}
public static void setColourDb(ColourDB colourDb) {
COLOUR_DB = colourDb;
}
}
Теперь мы можем заменить Color DB в loadDataFromDb с помощью DB.getColourDb() . Теперь код выглядит следующим образом
private static void loadDataFromDb() {
List colourData = DB.getColourDb().getColours();
for (ColourDB.ColourData colourDatum : colourData) {
map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum));
}
}
Теперь, когда мы переработали его, мы можем успешно издеваться над ним.
Вы можете увидеть тестовый класс Цветовой тест следующим образом
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ColourTest {
public static final String BLACK = "BLACK";
public static final String WHITE = "WHITE";
public static final String YELLOW = "YELLOW";
public static final String RED = "RED";
@Mock
ColourDBImpl colourDB;
@InjectMocks
DB db;
@BeforeAll
void setUp() {
MockitoAnnotations.openMocks(this);
ColourDB.ColourData black = new ColourDB.ColourData(BLACK, 255, 255, 255, 3);
ColourDB.ColourData white = new ColourDB.ColourData(WHITE, 0, 0, 0, 4);
ColourDB.ColourData yellow = new ColourDB.ColourData(YELLOW, 255, 255, 0, 5);
Mockito.when(colourDB.getColours()).thenReturn(List.of(black, white, yellow));
}
@Test
void test_values() {
Colour[] values = Colour.values();
assertEquals(Colour.RED.name(), values[0].name());
assertEquals(Colour.valueOf(YELLOW).name(), values[values.length - 1].name());
assertTrue(Arrays.stream(values).anyMatch(colour -> colour.name().equals(Colour.RED.name())));
assertTrue(Arrays.stream(values).anyMatch(colour -> colour.name().equals(Colour.valueOf(WHITE).name())));
assertEquals(6, values.length);
}
@Test
void test_if_instances_are_same() {
assertSame(Colour.RED, Colour.valueOf(RED));
assertSame(Colour.valueOf(RED), Colour.valueOf(RED));
assertEquals(Colour.valueOf(RED), Colour.valueOf(RED));
assertSame(Colour.valueOf(WHITE), Colour.valueOf(WHITE));
assertEquals(Colour.valueOf(WHITE), Colour.valueOf(WHITE));
}
@Test
void test_ordinal() {
assertEquals(0, Colour.RED.ordinal()); // static
assertEquals(5, Colour.valueOf(YELLOW).ordinal()); // dynamic
}
@Test
void test_invalid_colour() {
String magenta = "MAGENTA";
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Colour.valueOf(magenta));
assertEquals("No Colour by the name " + magenta + " found", exception.getMessage());
}
@Test
void test_compareTo() {
int red = Colour.RED.compareTo(Colour.valueOf("WHITE"));
assertTrue(red < 0);
int yellow = Colour.valueOf("YELLOW").compareTo(Colour.RED);
assertTrue(yellow > 0);
}
@Test
void test_name() {
assertEquals("RED", Colour.RED.name());
assertEquals("YELLOW", Colour.valueOf("YELLOW").name());
}
}
Вам нужно будет использовать @beforeAll с @testInstance(testInstance. Жизненный цикл. ЗА_КЛАСС) поскольку статический блок выполняется только один раз. В противном случае Mockito выдаст Ненужное исключение заглушки , потому что макет не будет выполнять каждый тест.
Шаг 6: Сериализация!
Мы не рассматривали, что произойдет, если мы сериализуем класс Color. Константы перечисления сериализуются иначе, чем обычные сериализуемые или экстернализуемые объекты. Сериализуется только имя поля, а при десериализации используется valueOf() метод для возврата константы Enum.
Перечисления фактически являются одиночными числами. Однако свойство singleton нашего класса может быть нарушено во время десериализации. Таким образом, нам нужно убедиться, что мы также сохраняем одноэлементное свойство Color . Мы можем сделать это, внедрив readResolve 1 способ. Это гарантирует, что мы получим только экземпляр того же класса, что и тот, который мы уже создали. Мы уже храним название цвета. Поэтому, когда создается новый объект, он все равно будет возвращать уже существующие объекты, которые мы ожидаем.
public final class Colour implements Comparable, Serializable { // Existing code // // private Object readResolve() { return Colour.valueOf(colourName); } }
Мы можем добавить тест и проверить, сработает ли это.
@Test
void test_serialization_deserialization() throws IOException, ClassNotFoundException {
serialize(Colour.valueOf(BLACK));
Colour black = deserialize();
assertNotNull(black);
assertEquals(Colour.valueOf(BLACK), black);
assertSame(Colour.valueOf(BLACK), black);
serialize(Colour.RED);
Colour red = deserialize();
assertNotNull(red);
assertEquals(Colour.valueOf(RED), red);
assertSame(Colour.valueOf(RED), red);
assertEquals(Colour.RED, red);
assertSame(Colour.RED, red);
}
void serialize(Colour colour) throws IOException {
try (FileOutputStream fos = new FileOutputStream("data.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(colour);
}
}
Colour deserialize() throws IOException, ClassNotFoundException {
try (FileInputStream fis = new FileInputStream("data.obj");
ObjectInputStream ois = new ObjectInputStream(fis)) {
return (Colour) ois.readObject();
}
}
Это так и есть!:)
Ограничения
Как вы знаете, это имеет ограничение, заключающееся в том, что вы не можете статически выводить константы перечисления, кроме тех, которые определены внутри Цвет . Значения в базе данных должны быть указаны через Color.value Of() . Более того, вам потребуется изменить любые операторы switch-case, чтобы использовать значение константы вместо статических типов перечисления, поддерживаемых switch-case. Пример:
switch (Colour.RED.name()) {
case "RED" :
System.out.println("RED");
break;
default:
}
Я обнаружил, что это также известно как типобезопасное перечисление, которое использовалось до того, как Enum as types были введены в Java 5. Конечно, это на ступень ниже, чем перечисления, но… ты знаешь.
Однако вышесказанное – это взлом. В идеальном мире вам никогда не пришлось бы пытаться реализовать эти хаки. Однако, если вы окажетесь в таком положении, вы знаете, что делать. Пожалуйста, попробуйте провести рефакторинг и удалить этот код.
Вы можете найти код для проекта в моем репозитории Github: dynamic-enums |/.
Сделано! Теперь мы создали “Динамическое перечисление”! 😎
Теперь вы можете покоиться с миром и желать/надеяться, что следующий разработчик, коснувшийся этого фрагмента кода, не попытается связаться с вами.
Ссылки и материалы для чтения:
- Java Magazine учебное пособие по перечислениям
- Как использовать безопасные для типов перечисления в Java
- Остерегайтесь Java typesafe перечислений
- Подробнее о типобезопасных перечислениях
- Приемы перечисления: Динамические перечисления
- Введение в сериализацию Java
Оригинал: “https://dev.to/darshitpp/dynamic-enums-2ka7”