Функциональное программирование в ООП

в 14:08, , рубрики: clean code, ооп, Проектирование и рефакторинг, Совершенный код, функциональное программирование, метки: , ,

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

Пример

Давайте рассмотрим следующий класс (тематика несколько необычная, но надеюсь, что это сделает процесс чтения чужого кода менее скучным):

public class Invasion {
	private SearchCriterion[] _searchCriterion = (new GeniousCriterion()).or(new SexyCriterion());
	private ExperimentProcedure[] _experimentProcedure = (new MockProcedure()).then(new WatchProcedure());

	private _experience = new Experience();

	private HumanBeing[] _humans;
	private ArrayList<HumanBeing> _chosenOnes;

	public void investigate() {
		updateChosenOnesList();
		experiment();
	}

	private void updateChosenOnesList() {
		_chosenOnes.clear();

		for (HumanBeing human : _humans)
			if (_searchCriterion.satisfied(human))
				_chosenOnes.add(human);
	}

	private void experiment() {
		for (HumanBeing human : _chosenOnes)
			_experience.push(_experimentProcedure.do(human));
	}
}

В котором

Требуется выяснить, что делает метод investigate или убедиться в правильности его логики. При этом не хотелось бы разбираться в коде всех методов, которые он вызывает.
Метод updateChosenOnesList. В данном случае по названию метода можно определить, что его реализация должна обновить поле _chosenOnes. При этом, просмотрев список полей объекта, можно предположить, что искать новеньких будут среди _humans, а для поиска может использоваться критерий _searchCriterion. Однако проверять список полей объекта и прослеживать связь между их названиями и названиями методов не очень удобно. Аналогичная ситуация с методом experiment.

И еще пример

Вот другой вариант реализации этого класса (см. комментарии):

public class Invasion {
	// …..........

	public void investigate() {
		// можно прочитать не задумываясь: ищем счастливчиков среди людей по критерию, добавляем в избранное
		// вместо старого updateChosenOnesList();
		_chosenOnes = search(_searchCriterion, _humans);
		// ставим опыты над избранными, усваиваем собранный опыт
		_experience.push(experiment(_experimentProcedure, _chosenOnes));
	}

	private Experience experiment(ExperimentProcedure procedure, HumanBeing[] humans) {
		Experience exp = new Experience();

		for (HumanBeing human : humans)
			exp.push(procedure.do(human));

		return exp;
	}

	private ArrayList<HumanBeing> search(SearchCriterion criterion, HumanBeing[] humans) {
		ArrayList<HumanBeing> result = new ArrayList<HumanBeing>();

		for (HumanBeing human : humans)
			if (criterion.satisfied(human))
				result.add(human);

		return result;
	}
}

Собственно, это и было применение функционального стиля программирования. Давайте посмотрим, какие плюсы и минусы есть у такого варианта по сравнению с первым.

Плюсы

Их нет, зато есть куча минусов. Шутка.
Во-первых, теперь легче прочитать метод investigate и понять, что он делает, и делает ли он то, что должен, и если делает, то правильно ли. При этом можно не смотреть никуда кроме тела самого метода, в том числе и на список полей.
Во-вторых, теперь приватные методы не зависят от состояния объекта. Получается, что результат их выполнения определяется исключительно передаваемыми параметрами (т. е. при вызове не нужно следить за состоянием всех полей объекта Invasion — видно, какие поля передаются и какие модифицируются). В то же время, те параметры, которые используются в качестве аргументов, были сформированы / инициализированы в теле метода investigate — т.е. виден весь алгоритм и все изменения, которые произошли с состоянием Invasion.

Минусы

Производительность — увы. В тоже время, нет смысла печалиться, если только метод investigate не будет вызываться очень часто.
Надо передавать методам дополнительные параметры — это достаточно критично, если их оказывается много — 4 и больше. С другой стороны, если параметров, от которых зависит метод много, то, возможно, это повод задуматься об изменении архитектуры (обычно — добавлении нового класса, такого как SexyCriterion, который может содержать в себе нужные параметры).

По поводу примера

Вообще говоря, в данном случае не было необходимости в закрытых методах. Достаточно было добавить объектам критерия поиска и процедур возможность целиком обрабатывать коллекцию объектов. Однако здесь были по-максимуму распределены обязанности между разными типами объектов — в первую очередь, с целью сокращения объема кода для примера. Часто бывает, что на построение такой модели либо нет времени, либо нет необходимости (например, когда есть подозрение, что требования могут поменяться и нужно применить наиболее простой вариант решения). В таких случаях функциональный стиль может несколько упростить чтение и поддержку кода, при этом практически не увеличивая время, затраченное на разработку.

Эпилог

Можно писать код, который не содержит методов с большим числом строк (например, больше 10-ти) и при этом не требует приватных методов (такие методы всегда могут быть вынесены в отдельный объект поведения). Однако, процесс построения такого кода не прост.
Поэтому, можно разбить сложный код public метода на несколько private функций-методов, зависящих исключительно от своих параметров, и не модифицирующих состояние объекта. В таком случае, напрямую к состоянию объекта должны обращаться-модифицировать только public методы (не private) — т.е. методы, которые являются частью его интерфейса. Это позволяет свести сложный алгоритм к набору методов, каждый из которых может быть легко прочитан, при условии что переменные, методы и их параметры получили подходящие имена.
И, на всякий случай, если вдруг есть вопросы, где здесь ООП, а где функциональное программирование. ООП — на основе этой парадигмы разбиты обязанности между объектами Invasion, Criterion, Procedure, Experience. Функциональное программирование — с его помощью реализована внутренняя логика Invasion, приватные методы в данном случае являются функциями (они зависят только от входных параметров и не модифицируют состояние объекта Invasion).

Спасибо за внимание! Пожалуйста, пишите в комментах свои мнения, замечания и критику.

Автор: Sims

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js