Данный пост предлагает решение проблем с использованием перечислений при изменении состава констант или наличия дублирования кода при их использовании. В остальных случаях применение озвученного ниже подхода, как правило, нецелесообразно.
В Java, начиная с версии 1.5, помимо всего прочего появились так называемые перечисления (enum). Существует целый ряд плюсов от использования перечислений против именованных констант:
Однако по сравнению, допустим, с C++, перечисления в Java представляют собой полноценные объекты, что предоставляет разработчику гораздо большую гибкость.
Учитывая все вышеперечисленное, можно сказать, что перечисления в Java — это уже не просто константы (т.е. данные), а полноценные объекты, из чего можно сделать определенные выводы об области их использования.
При использовании предложенного ниже подхода необходимо учесть, что он достаточно сложен. В соответствии со следующим постом, данная конструкция не должна применяться в простых случаях.
Ее назначение — упростить жизнь в сложных ситуациях, ориентированных на расширение, таких, как:
При отсутствии данных симптомов применение предложенного ниже подхода сомнительно и даже опасно, т.к. увеличившаяся сложность кода требует более тщательного анализа при возникновении каких-либо ошибок.
Теперь, собственно, перейдем к сути вопроса, который хотелось рассмотреть.
Перечисления нужно использовать в тех случаях, для которых в плюсах применялись switch'и. Поскольку перечисления могут содержать наследуемые методы, а также имплементировать интерфейсы, всю логику обработчиков веток switch можно вынести в методы класса перечисления. Запомним это как факт, и рассмотрим следующую проблему.
Вследствие кажущейся простоты перечислений, а также того факта, что из опыта языков типа C++ мы привыкли рассматривать их как данные, перечисления управляют поведением системы в самых различных частях системы, как правило, слабо связанных друг с другом. При этом если выносить всю условную логику в методы перечислений (для избавления от ветвлений) неизбежно возникает насыщение и переполнение логики, содержащейся в классе перечисления. Также это приводит к повышению связности модулей.
В этом случае можно поступить следующим образом:
Таким образом получится подход, отличающийся следующими плюсами и минусами:
Все перечисленные минусы могут быть разрешены, если написать соответствующую поддержку данного функционала в виде плагина к используемой IDE. Это, конечно, требует некоторых затрат, но может быть решено централизовано и один раз, после чего использоваться всегда и везде.
Например, имеем следующий набор классов (в разных модулях):
Видно, что модули достаточно слабо связаны между собой.
Допустим, нам по требованию заказчика появляется новый статус документа — отправленный для подтверждения (VERIFY).
При добавлении нового статуса придется во всех предложенных местах добавлять новый case. При этом очень легко забыть его где-то добавить. Конечно, можно предусмотреть default-блоки, выкидывающие исключения, но для большой системы это не гарантирует, что все места будут замечены.
Предлагается преобразовывать этот код к следующему виду:
Видно, что клиентский код стал гораздо короче и понятнее.
Конечно, сами обработчики все равно нужно писать, но теперь при добавлении нового элемента перечисления его обработчики написать придется. Уже нет опасности, что про это забудут (намеренное вредительство и оставление «на потом» не рассматриваются), по-крайней мере хотя бы подумают. И, как уже говорилось, всегда можно завести обработчик по-умолчанию:
P.S. Жду от хабрасообщества комментариев по поводу обоснованности данного подхода. Если наберется достаточное количество голосов, готов реализовать этот плагин для IntelliJ IDEA, Eclipse и NetBeans.
UPD. Добавился раздел «Простота — это сила!» в ответ на соответствующий пост, чтобы показать его место в пределах данного подхода.
UPD 2. По многочисленным просьбам добавил пример. Также в начало поста добавил краткое описание применимости данного решения.
Описание перечислений в Java
В Java, начиная с версии 1.5, помимо всего прочего появились так называемые перечисления (enum). Существует целый ряд плюсов от использования перечислений против именованных констант:
- Компилятор гарантирует корректную проверку типов
- Удобство итерации по всем возможным значениям перечисления
- Они занимают меньше места в switch-блоке (не нужно указывать имя класса)
- и т.д.
Однако по сравнению, допустим, с C++, перечисления в Java представляют собой полноценные объекты, что предоставляет разработчику гораздо большую гибкость.
- Во-первых, все перечисления наследуются от класса java.lang.Enum, у которого есть ряд удобных методов, а именно:
— name() — имя константы в виде строки
— ordinal() — порядок константы (соответствует порядку, в котором объвлены константы)
— valueOf() — статический метод, позволяющий получить объект перечисления по классу и имени
- Далее, как уже было озвучено, у класса перечисления есть возможность получить все возможные значения перечисления путем вызова метода java.lang.Class.getEnumConstants() у класса перечисления
- В классе перечисления имеется возможность задавать конструкторы (только приватные), поля и методы
- Перечисления могут реализовывать любые интерфейсы
- При этом методы в перечислении могут быть абстрактными, а конкретные экземпляры констант могут определять такие методы (как, впрочем, и переопределять уже определенные)
Учитывая все вышеперечисленное, можно сказать, что перечисления в Java — это уже не просто константы (т.е. данные), а полноценные объекты, из чего можно сделать определенные выводы об области их использования.
Простота — это сила!
При использовании предложенного ниже подхода необходимо учесть, что он достаточно сложен. В соответствии со следующим постом, данная конструкция не должна применяться в простых случаях.
Ее назначение — упростить жизнь в сложных ситуациях, ориентированных на расширение, таких, как:
- Наличие нескольких слабо связанных модулей, использующих один и тот же класс перечисления
- Наличие прецедентов по изменению состава констант перечисления
- Наличие дублирования кода в switch-блоках, использующих данное перечисление
При отсутствии данных симптомов применение предложенного ниже подхода сомнительно и даже опасно, т.к. увеличившаяся сложность кода требует более тщательного анализа при возникновении каких-либо ошибок.
Использование перечислений в изменяющейся среде
Теперь, собственно, перейдем к сути вопроса, который хотелось рассмотреть.
Перечисления нужно использовать в тех случаях, для которых в плюсах применялись switch'и. Поскольку перечисления могут содержать наследуемые методы, а также имплементировать интерфейсы, всю логику обработчиков веток switch можно вынести в методы класса перечисления. Запомним это как факт, и рассмотрим следующую проблему.
Вследствие кажущейся простоты перечислений, а также того факта, что из опыта языков типа C++ мы привыкли рассматривать их как данные, перечисления управляют поведением системы в самых различных частях системы, как правило, слабо связанных друг с другом. При этом если выносить всю условную логику в методы перечислений (для избавления от ветвлений) неизбежно возникает насыщение и переполнение логики, содержащейся в классе перечисления. Также это приводит к повышению связности модулей.
В этом случае можно поступить следующим образом:
- Разбиваем класс перечисления на обработчики, каждый из которых будет соответствовать одному из модулей системы. Этим мы решаем проблему перегруженности интерфейса самого класса перечисления.
- Остается решить проблему связности. Для этого каждому обработчику ставим в соответствие интерфейс, а экземпляры будем получать через фабрику. Сама же фабрика может создаваться с использованием декларативного подхода, т.е. связь интерфейсов с реализациями будет осуществляться на уровне конфигурации (например, через xml).
Таким образом получится подход, отличающийся следующими плюсами и минусами:
- + Дублирование кода сводится с минимуму
- + Улучшается читабельность кода
- + Логика обработчиков может быть разделена каким угодно способом. Иерархия наследования обработчиков может быть любая
- + При добавлении новых обработчиков (модулей) или элементов enum'а ничего не будет забыто
- + Т.к. нет ограничений на иерархию обработчиков, всегда можно предусмотреть обработчики по-умолчанию
- — Повышенные затраты на кодирование
- — Усложнение структуры кода
- — Для случаев, когда необходимо в явном виде устранить зависимости между модулями (например, физическое разделение модулей), необходимо поддерживать конфигурацию фабрики в актуальном состоянии
Все перечисленные минусы могут быть разрешены, если написать соответствующую поддержку данного функционала в виде плагина к используемой IDE. Это, конечно, требует некоторых затрат, но может быть решено централизовано и один раз, после чего использоваться всегда и везде.
Пример использования
Например, имеем следующий набор классов (в разных модулях):
public enum DocumentStatus {
NEW(0),
DRAFT(1),
PUBLISHED(2),
ARCHIVED(3);
private DocumentStatus(int statusCode) {
this.statusCode = statusCode;
}
public int getStatusCode() {
return statusCode;
}
private int statusCode;
}
// Web
public class DocumentWorklfowProcessor {
...
public List<Operation> getAvailableOperations(DocumentStatus status) {
List<Operation> operations;
switch (status) {
case NEW:
operations = ...;
break;
case DRAFT:
operations = ...;
break;
case PUBLISHED:
operations = ...;
break;
case ARCHIVED:
operations = ...;
break;
}
return operations;
}
public void doOperation(DocumentStatus status, Operation op) throws APIException {
switch (status) {
case NEW:
// один набор параметров операции
break;
case DRAFT:
// другой набор параметров операции
break;
case PUBLISHED:
// третий набор параметров операции
break;
case ARCHIVED:
// и т.д.
break;
}
}
}
// Scheduled task
public class ReportGenerationProcessor {
...
public void generateReport(Report report) {
DocumentStatus status = report.getDocument().getStatus();
ReportParams params;
switch (status) {
case NEW:
case DRAFT:
// params = одни критерии отбора отображаемых в отчете элементов
break;
case PUBLISHED:
// params = другие критерии отбора отображаемых в отчете элементов
break;
case ARCHIVED:
// и т.д.
break;
}
// Генерация отчета
}
}
Видно, что модули достаточно слабо связаны между собой.
Допустим, нам по требованию заказчика появляется новый статус документа — отправленный для подтверждения (VERIFY).
При добавлении нового статуса придется во всех предложенных местах добавлять новый case. При этом очень легко забыть его где-то добавить. Конечно, можно предусмотреть default-блоки, выкидывающие исключения, но для большой системы это не гарантирует, что все места будут замечены.
Предлагается преобразовывать этот код к следующему виду:
public interface IReportGeneratorProcessor {
public ReportParams getReportParams();
}
public interface IDocumentWorklfowProcessor{
public List<Operation> getAvailableOperations();
public void doOperation(Operation op) throws APIException;
}
public enum DocumentStatus {
// Здесь вместо new можно использовать фабрику или даже lazy-инициализацию в get-методах
NEW(0, new NewDocReportGeneratorProcessor(), new NewDocWorklfowProcessor()),
DRAFT(1, new DraftDocReportGeneratorProcessor(), new DraftDocWorklfowProcessor()),
PUBLISHED(2, ...),
ARCHIVED(3, ...);
private DocumentStatus(int statusCode, IReportGeneratorProcessor reportProc,
IDocumentWorklfowProcessor docProc) {
this.statusCode = statusCode;
this.reportProc = reportProc;
this.docProc = docProc;
}
public int getStatusCode() {
return statusCode;
}
public IReportGeneratorProcessor getReportGeneratorProcessor() {
return reportProc;
}
public IDocumentWorklfowProcessor getDocumentWorklfowProcessor() {
return docProc;
}
private int statusCode;
private IReportGeneratorProcessor reportProc;
private IDocumentWorklfowProcessor docProc;
}
// Web
public class DocumentWorklfowProcessor {
...
public List<Operation> getAvailableOperations(DocumentStatus status) {
return status.getDocumentWorklfowProcessor().getAvailableOperations();
}
public void doOperation(DocumentStatus status, Operation op) throws APIException {
status.getDocumentWorklfowProcessor().doOperation(op);
}
}
// Scheduled task
public class ReportGenerationProcessor {
...
public void generateReport(Report report) {
DocumentStatus status = report.getDocument().getStatus();
ReportParams params = status.getReportGeneratorProcessor().getReportParams();
// Генерация отчета
}
}
Видно, что клиентский код стал гораздо короче и понятнее.
Конечно, сами обработчики все равно нужно писать, но теперь при добавлении нового элемента перечисления его обработчики написать придется. Уже нет опасности, что про это забудут (намеренное вредительство и оставление «на потом» не рассматриваются), по-крайней мере хотя бы подумают. И, как уже говорилось, всегда можно завести обработчик по-умолчанию:
public IDocumentWorklfowProcessor getDocumentWorklfowProcessor() {
return (docProc != null) ? docProc : DEFAULT_WORKFLOW_PROCESSOR;
}
P.S. Жду от хабрасообщества комментариев по поводу обоснованности данного подхода. Если наберется достаточное количество голосов, готов реализовать этот плагин для IntelliJ IDEA, Eclipse и NetBeans.
UPD. Добавился раздел «Простота — это сила!» в ответ на соответствующий пост, чтобы показать его место в пределах данного подхода.
UPD 2. По многочисленным просьбам добавил пример. Также в начало поста добавил краткое описание применимости данного решения.
