8 октября 2012 в 18:08

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

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

Пример

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

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).


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

Добавлено 09/10/2012, с учетом некоторых комментариев
Пожалуйста, обратите внимание, что речь идет не только о редактировании состояния объекта внутренними (вспомогательными) методами класса, но еще и о том, что эти методы не должны считывать состояние объекта сами (все свои параметры они получают в качестве аргументов).
Естественно, этот принцип не претендует на абсолютность, есть случаи, когда закрытым методам лучше дать возможность обращаться к состоянию класса. В первую очередь, это случай, когда может возникнуть дублирование кода в виде передачи одного и того же набора аргументов функции в разных местах (либо редактирования одного и того же набора полей по результатам выполнения функции). Например, если метод updateChosenOnesList должен вызываться в нескольких местах и ему каждый раз надо передавать одни и те же внутренние поля объекта, то он определенно лучше реализован в первом примере.
Т.е. целью статьи является не указание способа кодирования, а обращение внимания на те плюсы, которые дает использование методов-функций.
Тема возможности редактирования функциями своих аргументов здесь НЕ рассматривалась.
–1
10642
39
Sims 0,5

Комментарии (22)

+3
meta4, #
Два тривиальных примера, не имеющие к ООП никакого отношения: описание классов абсолютно побоку. То, что аргументы в функции лучше не изменять — известный тезис, который к функциональному программированию не привязан. Собственно, всю вашу статью можно ужать до этого тезиса.
+1
Sims, #
По поводу тривиальности примеров — имхо, но примеры должны быть тривиальными по своей сути поскольку их задачей является упрощение усвоения информации, а не наоборот.
По поводу отношения к ООП — пожалуйста, уточните, потому что есть мнение, что оба приведенных примера соответствуют парадигме ООП по всем трем пунктам.
Изменение аргументов функций — речь здесь не об этом, а о том, что функции (в данном случае приватные методы) не зависят от внутреннего состояния объекта (его полей, если совсем конкретно) и не изменяют его.
0
meta4, #
>но примеры должны быть тривиальными
Да сама идея тривиальная, формулируется в одном предложении, из разряда прописных истин.

>По поводу отношения к ООП
То, что функции желательно не менять состояние параметров — к ООП не имеет никакого отношения.

>функций… не зависят от внутреннего состояния объекта
О том и речь. Функции, не зависящие от состояния объекта вообще не являются методами, и непонятно зачем они вообще методы объекта, а не класса. Зачем вообще тогда класс приведен — не ясно.
0
Sims, #
>но примеры должны быть тривиальными
Опыт показывает, что эта прописная истина все же ускальзывает от некоторых разработчиков, применяющих разделение власти и часто рефакторинг заканчивается разбиением большого метода на несколько маленьких, каждый из которых получает доступ к состоянию объекта. Что, во многом, равносильно добавления комментариев в код.
>По поводу отношения к ООП
Рассмотрено сочетание парадигм ООП и функционального программирования, отсюда название.
>функций… не зависят от внутреннего состояния объекта
«О том и речь. Функции, не зависящие от состояния объекта вообще не являются методами, и непонятно зачем они вообще методы объекта, а не класса. Зачем вообще тогда класс приведен — не ясно.»
Вы правы, их можно отнести к методам класса. Класс нужен для примеров, в обоих случаях модифицируется состояние объекта этого класса — в первом в приватных методах, во втором — в паблик.
0
deilux, #
Эванс в книге про DDD так и писал: избегайте методов с побочными эффектами (В его определении — методы, меняющие внутреннее состояние объекта). Как раз оно
0
deilux, #
И да, первый листинг… грубо говоря не очень удачен.
0
Sims, #
Уверен, что 99% процентов мыслей, которые возникнут в моей голове уже кто-то успел подумать и возможно даже изложить в письменном виде и опубликовать.
«И да, первый листинг… грубо говоря не очень удачен.» — спасибо, так и планировалось.
0
deilux, #
А зачем так и планировалось? Заходя в топик, я думал вы хотите показать преимущества функционального подхода и вариант «как надо», а вы в очередной раз показали «как не надо».
0
Sims, #
Все правильно, первый вариант — как не надо.
Вариант возникновения первого примера — результат «неполного» рефакторинга метода по принципу разделения власти.
Второй вариант — как надо.
Преимущества функционального подхода были описаны в разделе «Плюсы».
+1
Sims, #
Кстати, речь не только об изменении методами состояния объекта (побочных эффектах), но и о том, что приватные методы (хелперы) не должны обращаться к состоянию объекта сами — все свои параметры они получают в качестве аргументов.
Хотя такой принцип и не претендует на универсальность, его, в первую очередь, стоит применять в случаях, когда можно четко сформулировать задачу метода-хелпера (и, соответственно, назвать его).
+1
VolCh, #
А зачем эти методы делать методами, тем более методами объекта, а не класса (static)?

Если уж на это пошло, то методы должны быть обычными функциями, нет?
+1
deilux, #
Ну это тогда совсем функциональщина будет, а мы же фанаты ООП как никак
+1
VolCh, #
Не совсем. Я постоянно практикую обращение из паблик методов к функциями, хотя бы стандартной библиотеки, а часто создаю и свои, в том числе и реализующих бизнес-логику, если она прямо к объекту мало привязана или повторяется в объектах, которые сложно в одну иерархию ввести. Условно эти функции можно назвать хелперами, в ООП модель их сложно вписать с разумным обоснованием, потому и не вписываю, а оставляю просто функциями.
0
PQR, #
А если объединить эти методы в traits (PHP 5.4) подключать к тем или иным «базовым» классам — как считаете?
0
VolCh, #
Смысла не вижу. Всё таки в состав класса надо вводить функции, у которых хотя бы один параметр $this непосредственно. Пока очевидным предназначением traits мне видится реализация интерфейсов типа Iterable По сути traits+интерфейсы дают обычное множественное наследование, не без нюансов, но всё же.
0
PQR, #
Другой случай: а если этим функциям (просто функциям) нужен доступ к DI конетйнеру (например, в контейнере может лежать сервис доступа к базе). Вы будете одним из параметров передавать контейнер в функцию при каждом вызове?

Или вы объединили бы все такие функции в виде методов в trait, чтобы иметь возможность получать сервисы через $this->container->…?
0
VolCh, #
Да, если это просто функции. А вот с traits я слабо представляю как это реализовать. Ведь контейнер надо инициализировать, а это чаще всего в конструкторе происходит. Но в конструкторе не только типовые инициализации, чтобы и конструктор в traits засунуть.
+2
swwwfactory, #
На мой взгляд разбиение не только можно, но и нужно. Вы правильно подметили сходство с функциональным подходом, что можно писать приватные методы, не изменяющие состояние объекта. В итоге метод, породивший вызовы приватных (добавил бы от себя вспомогательных или «дочерних») методов, принимает или отвергает «окончательное решение об изменении состояния». Таким образом данный прием позволяет локализовать точку изменения состояния объекта. Если во время выполнения приватного метода произойдет ошибка: let it crash… Важно также, чтобы «главный метод» не менял пошагово состояние объекта, а делал это атомарно в конце. Такое-же разбиение можно делать не только для публичных методов, но и везде, где уместно, включая дробление приватных методов. Такую программу легче отлаживать и повторно использовать. В особенности если есть интерфейсы.

Проблемы могут возникнуть при накоплении «вычисляемого состояния» — усложнение входных/выходных параметров в приватных методах. Если методов много — затрудняет отладку. Легко увлечься программированием ради программирования. Гораздо важнее сосредоточиться не на коде, а на данных, структурах и взаимоотношениях между ними. Если это будет правильно, код сам ляжет как надо )))… постепенно… может быть… )))

Еще один интересный эффект данного подхода: все «приватные методы» могут быть static, впрочем об этом уже говорилось в комментариях выше.

К данному подходу можно добавить еще простые правила: код метода должен максимально умещаться на одной условной странице экрана редактора и не только по высоте ))) Если это не так: требуется разбиение либо что-то явно неправильно. Любой метод должен быть заточен на выполнение одной функциональности: «do one job»
0
tvolf, #
В первом листинге в _experience помещаются непосредственно результаты опытов над выбранными людьми:

_experience.push(_experimentProcedure.do(human));

, а во втором в _experiencе помещается другой объект типа Experience, который возвращается из метода experiment().

_experience.push(experiment(_experimentProcedure, _chosenOnes));

То есть, эти две функции не эквивалентны (если я правильно понял код =)
0
Sims, #
Эти два примера эквивалентны по результату своей работы, но во втором листинге функция experiment не обращается к полям объекта Invasion и не редактирует их. Контроль над состоянием объекта находится у функции investage, она же решает, какие поля нужно передать в experiment. Кроме того, в первом листинге метод investigate несет намного меньше смысловой нагрузки, по сравнению с методами updateChosenOnesList и experiment (что на мой взгляд так же является признаком плохого кода). Во втором листинге этот недостаток также исправлен.
0
tvolf, #
Я имею ввиду, нет ли во втором случае лишней операции push?
0
Sims, #
Не, во втором листинге все верно =) здесь подразумевается, что объект Experience умеет обрабатывать push как результата выполнения ExperimentProcedure, так и другого такого же объекта (результат второй операции — добавление всего опыта из передаваемого объекта Experience). Т.е. в функции experiment создается и наполняется объект Experience, который затем добавляется в поле _experience.

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