Данный пост предлагает решение проблем с использованием перечислений при изменении состава констант или наличия дублирования кода при их использовании. В остальных случаях применение озвученного ниже подхода, как правило, нецелесообразно.
В 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. По многочисленным просьбам добавил пример. Также в начало поста добавил краткое описание применимости данного решения.