Статьи

Почему я функтофоб


В
мире PHP
распространено мнение, что иногда нам лучше определять функции в глобальном пространстве имен, чем организовывать код в классы. Вот мой взгляд на проблемы, которые возникают, когда вы пишете даже очень мало определений ‘function foo ()’.

декомпозиция

Именованные функции в одном пространстве имен являются сущностью процедурной декомпозиции: do A, затем B, затем C; чтобы сделать A, выполните A1, A2 и A3 в последовательности. Передайте состояние программы от одной процедуры к другой с некоторой общей структурой данных, такой как глобальная переменная.

В результате мы имеем много изолированных функций (представьте собственные функции массива _ * ()) и некоторую структуру данных, которой они манипулируют. Однако вы не можете полагаться на многое в структуре данных, поскольку нет инкапсуляции: в любой точке программы вы не можете быть уверены, что массив является неизменным, что все его значения имеют одинаковый тип или что он имеет фиксированное количество элементов.

На этой картинке очень мало инкапсуляции: каждая функция может модифицировать каждую часть головоломки. Если вы попытаетесь избежать глобальных переменных и синглетонов, вы обнаружите, что в процедурной декомпозиции очень сложно, так как эти изолированные функции нуждаются в некотором контексте для правильной работы, который импортируется с помощью глобального оператора.

Однако самая важная проблема заключается в том, что функции вызывают другие функции напрямую:

function foo($argument)
{
    // do some stuff...
    return bar($argument, 'something else');
}

так что практически невозможно протестировать foo и bar по отдельности (модульное тестирование) или создать foo () с другим bar ().

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

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

Можно ввести функцию, поэтому есть швы, которые вы можете использовать для тестирования или замены:

$foo = function($argument, $bar) {
    // do some stuff
    return $bar($argument, 'something else');
};

Если вы посмотрите на другие динамические языки, вы увидите, что даже когда функции фактически определены с именем (один из случаев JavaScript), их можно передавать в виде значений:

function foo() { ... }
var myFunction = foo;

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

Объектно-ориентированный

Это немного вводит в заблуждение, что мы искушенные разработчики помещаем все в классы ; в ОО-разложении мы помещаем все на объекты .

Таким образом, не существует изолированных функций, которые связываются со структурами данных, а только методы, реализованные на объектах в целом. Те соединяются с протоколами, выраженными явно в интерфейсах или неявно в типизации утки.

Если вы работаете с OO-декомпозицией, вы принимаете, что логика сама по себе не имеет смысла, а только привязана к объекту. В случае функций массива они помещаются в сам массив, который из-за наследства PHP является примитивным значением вместо объекта.

class Foo
{
    public function __construct(Bar $bar)
    {
        $this->bar = $bar;
    }

    public function __invoke($argument)
    {
        // do some stuff...
        $this->bar->__invoke($argument, 'something else');
    }
}

(просто используйте __invoke здесь на месте, поскольку у нас нет реальных имен в этих примерах.)

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

Функциональный PHP?

Существует еще одна проблема с анонимными функциями, реализованными в PHP, даже если они могут заменить подход ОО.
В PHP у нас есть (очень необязательная) система типов. Объекты могут работать как с утиной типизацией, так и с Java-подобными интерфейсами:

interface Collation
{
    /**
     * @return int -1, 0 or 1. -1 if $char precedes $another
     */
    function compare($char, $another);
}

Теперь я могу использовать подсказку типа сортировки:

class Collection
{
    public function sort(Collation $c) { ... }
}

В других случаях я предпочитаю печатать на утке:

class Collection
{
    public function sort($c) { ... }
}

и я надеюсь, что мои тесты поймут, если я вызову несуществующий метод для объектов $ c.

Но не существует эквивалента интерфейсов для анонимных функций, и вам остается ввести утку:

$compare = function($char, $another) { ... };
$sort = function($collection, $c) { ... };

Нельзя сказать, что когда вы передаете Closure в качестве аргумента некоторому методу или другой функции, он должен соблюдать определенный протокол.

Но сколько аргументов принимает $ c? Из каких типов? Должен ли он что-то вернуть? Где мы выражаем все эти вещи без дублирования, отдельно от сотен мест, где мы создаем анонимные функции $ c? И отдельно от сотен мест, где мы принимаем анонимные функции $ c?

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