Pull to refresh

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

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



Описание перечислений в 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. По многочисленным просьбам добавил пример. Также в начало поста добавил краткое описание применимости данного решения.
Tags:
Hubs:
+3
Comments 9
Comments Comments 9

Articles