Я сейчас работаю над кодом, которому для работы требуется список параметров функции:
/**
* @param list<ReflectionParameter> $parameters
*/
function make(array $parameters): Something {}
На первый взгляд, всё здорово.
list<ReflectionParameter> выражает минимально необходимое знание для решения задачи да и вообще это список из объектов-значений, а не каких-то там примитивов.Но есть нюанс.
В такой
make можно передать невозможный список параметров, например:
$trimReflection = new ReflectionFunction(trim(...));
make(array_reverse($trimReflection->getParameters()));
С точки зрения типов всё верно, но инвариант "опциональные параметры идут после обязательных" в переданном списке нарушен, что может привести к неправильной работе функции. Можно передать параметры от разных функций (тогда
$parameters[$i]->getDeclaringFunction() будет давать неконсистентный результат), можно поставить вариадик в начало — способов задать неверный список много, потому что list<ReflectionParameter> ничего толком не регламентирует.Если функция работает с концепцией "список параметров валидной сигнатуры", она должна принимать
ReflectionFunctionAbstract и сама вызывать getParameters():
function make(ReflectionFunctionAbstract $function): Something
{
$parameters = $function->getParameters();
// ...
}
Теперь в теле
make можно быть уверенным, что список параметров удовлетворяет всем инвариантам, ведь отрефлексировать функцию с неверной сигнатурой не получится...Хотя подождите... Можно же так поломать:
final class MessedReflectionFunction extends ReflectionFunction
{
public function getParameters(): array
{
return array_reverse(parent::getParameters());
}
}
make(new MessedReflectionFunction(trim(...)));
Но это уже разговор про LSP и что наследники не должны нарушать инварианты родителей — тема для другого поста. 😊
Мораль такая: принимайте не просто минимум знаний, а минимум, гарантирующий необходимые инварианты. Это и есть концепция Whole Value.
⸻
Этот пост я разместил неделю назад в 🐘 PHPeople.