Несколько слов об использовании перечислений в изменяющейся среде

    Данный пост предлагает решение проблем с использованием перечислений при изменении состава констант или наличия дублирования кода при их использовании. В остальных случаях применение озвученного ниже подхода, как правило, нецелесообразно.



    Описание перечислений в Java


    В Java, начиная с версии 1.5, помимо всего прочего появились так называемые перечисления (enum). Существует целый ряд плюсов от использования перечислений против именованных констант:
    • Компилятор гарантирует корректную проверку типов
    • Удобство итерации по всем возможным значениям перечисления
    • Они занимают меньше места в switch-блоке (не нужно указывать имя класса)
    • и т.д.

    Однако по сравнению, допустим, с C++, перечисления в Java представляют собой полноценные объекты, что предоставляет разработчику гораздо большую гибкость.
    • Во-первых, все перечисления наследуются от класса java.lang.Enum, у которого есть ряд удобных методов, а именно:
      — name() — имя константы в виде строки
      — ordinal() — порядок константы (соответствует порядку, в котором объвлены константы)
      — valueOf() — статический метод, позволяющий получить объект перечисления по классу и имени
    • Далее, как уже было озвучено, у класса перечисления есть возможность получить все возможные значения перечисления путем вызова метода java.lang.Class.getEnumConstants() у класса перечисления
    • В классе перечисления имеется возможность задавать конструкторы (только приватные), поля и методы
    • Перечисления могут реализовывать любые интерфейсы
    • При этом методы в перечислении могут быть абстрактными, а конкретные экземпляры констант могут определять такие методы (как, впрочем, и переопределять уже определенные)

    Учитывая все вышеперечисленное, можно сказать, что перечисления в Java — это уже не просто константы (т.е. данные), а полноценные объекты, из чего можно сделать определенные выводы об области их использования.

    Простота — это сила!


    При использовании предложенного ниже подхода необходимо учесть, что он достаточно сложен. В соответствии со следующим постом, данная конструкция не должна применяться в простых случаях.

    Ее назначение — упростить жизнь в сложных ситуациях, ориентированных на расширение, таких, как:
    • Наличие нескольких слабо связанных модулей, использующих один и тот же класс перечисления
    • Наличие прецедентов по изменению состава констант перечисления
    • Наличие дублирования кода в switch-блоках, использующих данное перечисление


    При отсутствии данных симптомов применение предложенного ниже подхода сомнительно и даже опасно, т.к. увеличившаяся сложность кода требует более тщательного анализа при возникновении каких-либо ошибок.

    Использование перечислений в изменяющейся среде


    Теперь, собственно, перейдем к сути вопроса, который хотелось рассмотреть.

    Перечисления нужно использовать в тех случаях, для которых в плюсах применялись switch'и. Поскольку перечисления могут содержать наследуемые методы, а также имплементировать интерфейсы, всю логику обработчиков веток switch можно вынести в методы класса перечисления. Запомним это как факт, и рассмотрим следующую проблему.

    Вследствие кажущейся простоты перечислений, а также того факта, что из опыта языков типа C++ мы привыкли рассматривать их как данные, перечисления управляют поведением системы в самых различных частях системы, как правило, слабо связанных друг с другом. При этом если выносить всю условную логику в методы перечислений (для избавления от ветвлений) неизбежно возникает насыщение и переполнение логики, содержащейся в классе перечисления. Также это приводит к повышению связности модулей.

    В этом случае можно поступить следующим образом:
    1. Разбиваем класс перечисления на обработчики, каждый из которых будет соответствовать одному из модулей системы. Этим мы решаем проблему перегруженности интерфейса самого класса перечисления.
    2. Остается решить проблему связности. Для этого каждому обработчику ставим в соответствие интерфейс, а экземпляры будем получать через фабрику. Сама же фабрика может создаваться с использованием декларативного подхода, т.е. связь интерфейсов с реализациями будет осуществляться на уровне конфигурации (например, через 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. По многочисленным просьбам добавил пример. Также в начало поста добавил краткое описание применимости данного решения.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 9
    • +8
      Я бы предложил Вам добавить пример для увеличения наглядности.
      • +3
        ага, примеры нужны. я, например, не вкурил зачем нужен плагин.
        • 0
          Можно автоматизировать следующие вещи:
          • Автоматизированный рефакторинг существующего кода — ручками это делать всегда утомительно, да и ошибок можно внести, пока перетаскиваешь код.
          • Для случая с использованием фабрик и xml-конфигурации — гарантия того, что для каждой константы и каждого интерфейса в ней предусмотрен обработчик, прописанный в xml-конфигурации.
            Как всегда в декларативном программировании: если нет поддержки IDE, то гарантия консистентности превращается в ад программиста.

        • 0
          Статья антипод: habrahabr.ru/blogs/arbeit/99889/
          • 0
            Да нет, это не антипод. Перечисления — это уже не шаблон, а простая конструкция языка. Такой же, как if, else, switch.
            В той статье-«антиподе» говорится, что не надо использовать сложные средства для простых задач без явной необходимости.
            Из этой же статьи вообще не совсем понятно, когда использовать enum нужно и не нужно.
          • 0
            Действительно, как минимум нужны примеры, иначе остаётся совершенно непонятным, что это такое и когда это нужно.
            • 0
              С примером полёт мысли вроде вполне понятен. Правда глядя на проблему совсем уж сверху, видна довольно узкая ниша этого решения, что впрочем автор и отмечает.

              Когда-то я решал похожую(на твой пример) задачу и в итоге пришёл к выводу что в большинстве случаев когда возникают похожие проблемы/вопросы, то надо посмотреть в сторону Guice ну или прочих DI(dependency injection) фреймворков. Конечно не всегда и не каждому это подойдёт(да и не всем дадут менять настолько глобально то что и так работает), но наверное стоит отметить такой вариант тоже где-нибудь в послесловии — пусть люди смотрят уже что им лучше в каждом конкретном случае.
              • 0
                Если можно, то енумы лучше не использовать, а то код постепенно превращается в спагетти ifelse/switch выражений.
                • 0
                  Путь правильный, только чуть-чуть не додумали. Кроме двух неочевидно необходимых интерфейсов, попали еще и на дополнительные циклические связи: если энум в одном джаре, веб в другом, а шедулер — в третьем, то ничего путного так не соберется.

                  Визитор (в разных видах) имхо самый элегантный способ решения проблемы декаплинга с одной стороны и компайл тайм гарантий что ничего не забыто с другой. В данном случае, к примеру, можно было бы сделать так: энум реализует интерфейс (возможность джавы, о которой незаслужено забывают) с одним методом accept(DocumentStatusVisitor). DocumentStatusVisitor — это интерфейс, объявленный в том же модуле, что и энум и в нем мы переходим от энума (конструкции процедурной) к методам (конструкции ООП) — для каждого элемента энума создаем именной метод, к примеру visitNew(), visitDraft(),… Ну что написать в имлементациях метода accept в энуме я думаю понятно.

                  Таким образом мы избавляемся от лишних депенденсий (энум больше не должен знать кто там и как его использует), получаем возможность держать моули раздельно, уменьшаем кол-во интерфейсов и связей.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.