Статьи

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

Новая реклама в программировании — это парадигмы функционального программирования. Функциональные языки все больше и больше используются в больших и лучших приложениях. Scala, Haskel и т. Д. Процветают, и другие, более консервативные языки, такие как Java, начали использовать некоторые из парадигм функционального программирования (см. Замыкания в Java7 и lazy eval для списков в Java8). Однако мало кто знает, что PHP достаточно универсален, когда речь заходит о функциональном программировании. Все основные концепции функционального программирования могут быть выражены в PHP. Так что, если вы новичок в функциональном программировании, будьте готовы к тому, что вы будете поражены, а если вы уже знакомы с функциональным программированием, будьте готовы повеселиться с этим учебником.


Без каких-либо парадигм программирования мы могли бы делать все, что мы хотим, так, как мы хотим. Хотя это приведет к чрезвычайной гибкости, это также приведет к невозможным архитектурам и очень раздутому коду. Таким образом, парадигмы программирования были изобретены, чтобы помочь нам, программистам, осмысливать конкретную программу определенным образом и таким образом ограничивать нашу способность выражать свое решение.

Каждая парадигма программирования отнимает у нас свободу:

  • Модульное программирование убирает неограниченный размер программы.
  • Структурное и процедурное программирование устраняет необходимость и ограничивает программиста в последовательности, выборе и итерации.
  • Объектно-ориентированное программирование убирает указатели на функции.
  • Функциональное программирование снимает назначение и изменяемое состояние.

В функциональном программировании у вас нет данных, представленных переменными.

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

В объектно-ориентированном программировании все является объектом. И объект — это набор данных и методов, которые выполняют действия с этими данными. Объекты имеют состояние, изменчивое, изменчивое состояние.

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

Поскольку у вас нет значений, присвоенных переменным, в функциональном программировании у вас нет состояния .

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

Но если мы хотим выразить все в функциях, мы должны иметь возможность назначать их параметрам или возвращать их из других функций. Таким образом, функциональное программирование требует поддержки функций высшего порядка. Это в основном означает, что функция может быть назначена «переменной», отправлена ​​в качестве параметра другой функции и возвращена в результате выполнения функции.

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


Достаточно разговоров и философии для одного урока. Давайте код!

Настройте проект PHP в вашей любимой IDE или редакторе кода. Создайте в нем папку "Tests" . Создайте два файла: FunSets.php в папке проекта и FunSetsTest.php в папке Tests. Мы создадим приложение с тестами, которое будет представлять концепцию множеств.

В математике множество — это совокупность отдельных объектов, рассматриваемых как объект сам по себе. (Википедия)

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

Итак, давайте код! Но ждать. Как? Что ж, чтобы уважать концепции функционального программирования, мы должны будем применить следующие ограничения к нашему коду:

  • Нет назначений. — Нам не разрешено присваивать значения переменным. Однако мы можем назначать функции переменным.
  • Нет изменяемого состояния. — В случае назначения нам не разрешено изменять значение этого назначения. Нам также не разрешено изменять значение любой переменной, значение которой было установлено в качестве параметра для текущей функции. Таким образом, без изменения параметров.
  • Нет пока и для петель. — Нам не разрешено использовать PHP-команды «while» и «for». Однако мы можем определить наш собственный метод для циклического перемещения по элементам набора и вызова его foreach / for / while.

На тесты не распространяются никакие ограничения. Из-за природы PHPUnit мы будем использовать классический объектно-ориентированный PHP-код. Также, чтобы лучше соответствовать нашим тестам, мы свернем весь наш производственный код в один класс.

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

Определяющей функцией набора является метод «содержит».

1
2
3
function contains($set, $elem) {
    return $set($elem);
}

ОК … Это не так очевидно, поэтому давайте посмотрим, как бы мы это использовали.

1
2
$set = function ($element) {return true;};
contains($set, 100);

Ну, это объясняет это немного лучше. Функция "contains" имеет два параметра:

  • $set — представляет набор, определенный как функция.
  • $elem — представляет элемент, определенный как значение.

В этом контексте все, что "contains" , это применяет функцию в $set с параметром $elem . Давайте завернем все в тесте.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class FunSetsTest extends PHPUnit_Framework_TestCase {
 
    private $funSets;
 
    protected function setUp() {
        $this->funSets = new FunSets();
    }
 
    function testContainsIsImplemented() {
        // We caracterize a set by its contains function.
 
        $set = function ($element) {return true;};
        $this->assertTrue($this->funSets->contains($set, 100));
    }
}

И оберните наш производственный код внутри FunSets.php в класс:

1
2
3
4
5
6
class FunSets {
 
    public function contains($set, $elem) {
        return $set($elem);
    }
}

Вы можете запустить этот тест, и он пройдет. Набор, который мы определили для этого теста, это просто функция, которая всегда возвращает true. Это «истинный набор».

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

1
2
3
4
5
6
7
function testSingletonSetContainsSingleElement() {
    // A singleton set is characterize by a function which passed to contains will return true for the single element
    // passed as its parameter.
 
    $singleton = $this->funSets->singletonSet(1);
    $this->assertTrue($this->funSets->contains($singleton, 1));
}

Нам нужно определить функцию с именем "singeltonSet" с параметром, представляющим элемент множества. В тесте это номер один (1). Затем мы ожидаем, что наш метод contains , когда вызывается с помощью одноэлементной функции, чтобы вернуть true если переданный в параметре равен единице. Код для прохождения теста выглядит следующим образом:

1
2
3
4
5
public function singletonSet($elem) {
    return function ($otherElem) use ($elem) {
                return $elem == $otherElem;
            };
}

Вот это да! Это безумие. Итак, функция "singletonSet" получает в качестве параметра элемент как $elem . Затем он возвращает другую функцию с параметром $otherElem и эта вторая функция сравнивает $elem с $otherElem .

Так как же это работает? Во-первых, эта строка:

1
$singleton = $this->funSets->singletonSet(1);

превращается в то, что возвращает "singletonSet(1)" :

1
2
3
$singleton = function ($otherElem) {
       return 1 == $otherElem;
   };

Тогда "contains($singleton, 1)" называется. Который, в свою очередь, называет все, что находится в $singleton . Таким образом, код становится:

1
$singleton(1)

Который фактически выполняет код в нем с $otherElem имеющим значение один.

1
return 1 == 1

Что, конечно, верно, и наш тест проходит.

Ты уже улыбаешься? Вы чувствуете, что ваш мозг начинает кипеть? Я, конечно, сделал, когда я впервые написал этот пример в Scala, и я сделал это снова, когда я впервые написал этот пример на PHP. Я думаю, что это необычно. Нам удалось определить набор из одного элемента с возможностью проверки того, что он содержит значение, которое мы передали ему. Мы сделали все это без единого присвоения значения. У нас нет переменной, содержащей значение один или имеющей состояние один. Нет состояния, нет присваивания, нет изменчивости, нет петель. Мы на правильном пути здесь.


Теперь, когда мы можем создать набор с одним значением, нам нужно иметь возможность создать набор с несколькими значениями. Очевидный способ сделать это — определить операцию объединения для наших множеств. Объединение двух одноэлементных множеств будет представлять другое объединение с обоими значениями. Я хочу, чтобы вы потратили минуту и ​​подумали о решении, прежде чем переходить к коду, может быть, пиковое значение в тестах ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function testUnionContainsAllElements() {
    // A union is characterized by a function which gets 2 sets as parameters and contains all the provided sets
 
    // We can only create singletons at this point, so we create 2 singletons and unite them
    $s1 = $this->funSets->singletonSet(1);
    $s2 = $this->funSets->singletonSet(2);
    $union = $this->funSets->union($s1, $s2);
 
    // Now, check that both 1 and 2 are part of the union
    $this->assertTrue($this->funSets->contains($union, 1));
    $this->assertTrue($this->funSets->contains($union, 2));
    // … and that it does not contain 3
    $this->assertFalse($this->funSets->contains($union, 3));
}

Нам нужна функция с именем "union" которая получает два параметра, оба набора. Помните, наборы для нас просто функции, поэтому наша функция "union" получит две функции в качестве параметров. Затем, мы хотим иметь возможность проверить с помощью "contains" ли объединение элемент или нет. Таким образом, наша функция "union" должна возвращать другую функцию, которую может использовать "contains" .

1
2
3
4
5
public function union($s1, $s2) {
    return function ($otherElem) use ($s1, $s2) {
                return $this->contains($s1, $otherElem) ||
            };
}

Это на самом деле работает довольно хорошо. И это совершенно справедливо, даже если ваш союз называется с другим союзом плюс синглтон. Это вызывает contains внутри себя для каждого параметра. Если это союз, он будет повторяться. Это так просто.


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

01
02
03
04
05
06
07
08
09
10
11
public function intersect($s1, $s2) {
    return function ($otherElem) use ($s1, $s2) {
                return $this->contains($s1, $otherElem) && $this->contains($s2, $otherElem);
            };
}
 
public function diff($s1, $s2) {
    return function ($otherElem) use ($s1, $s2) {
                return $this->contains($s1, $otherElem) && !$this->contains($s2, $otherElem);
            };
}

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


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
function testFilterContainsOnlyElementsThatMatchConditionFunction() {
    $u12 = $this->createUnionWithElements(1, 2);
    $u123 = $this->funSets->union($u12, $this->funSets->singletonSet(3));
 
    // Filtering rule, find elements greater than 1 (meaning 2 and 3)
    $condition = function($elem) {return $elem > 1;};
 
    // Filtered set
    $filteredSet = $this->funSets->filter($u123, $condition);
 
    // Verify filtered set does not contain 1
    $this->assertFalse($this->funSets->contains($filteredSet, 1), «Should not contain 1»);
    // Check it contains 2 and 3
    $this->assertTrue($this->funSets->contains($filteredSet, 2), «Should contain 2»);
    $this->assertTrue($this->funSets->contains($filteredSet, 3), «Should contain 3»);
}
 
private function createUnionWithElements($elem1, $elem2) {
    $s1 = $this->funSets->singletonSet($elem1);
    $s2 = $this->funSets->singletonSet($elem2);
    return $this->funSets->union($s1, $s2);
}

Мы создаем набор из трех элементов: 1, 2, 3. И $u123 его в переменную $u123 чтобы его можно было легко идентифицировать в наших тестах. Затем мы определяем функцию, которую хотим применить к тесту, и помещаем ее в $condition . Наконец, мы вызываем фильтр для нашего набора $u123 с $condition и $u123 полученный набор в $filteredSet . Затем мы запускаем утверждения с помощью "contains" чтобы определить, выглядит ли набор так, как мы хотим. Наша функция условия проста, она вернет true, если элемент больше единицы. Таким образом, наш окончательный набор должен содержать только значения два и три, и это то, что мы проверяем в наших утверждениях.

1
2
3
4
5
6
7
public function filter($set, $condition) {
    return function ($otherElem) use ($set, $condition) {
                if ($condition($otherElem))
                    return $this->contains($set, $otherElem);
                return false;
            };
}

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


Следующим шагом является создание различных циклических функций. Первый из них, «forall ()» , примет $set и $condition и вернет true если $condition применяется ко всем элементам $set . Это приводит к следующему тесту:

01
02
03
04
05
06
07
08
09
10
11
function testForAllCorrectlyTellsIfAllElementsSatisfyCondition() {
    $u123 = $this->createUnionWith123();
 
    $higherThanZero = function($elem) { return $elem > 0;
    $higherThanOne = function($elem) { return $elem > 1;
    $higherThanTwo = function($elem) { return $elem > 2;
 
    $this->assertTrue($this->funSets->forall($u123, $higherThanZero));
    $this->assertFalse($this->funSets->forall($u123, $higherThanOne));
    $this->assertFalse($this->funSets->forall($u123, $higherThanTwo));
}

Мы извлекли создание $u123 из предыдущего теста в приватный метод. Затем мы определяем три различных условия: выше нуля, выше единицы и выше двух. Поскольку наш набор содержит числа один, два и три, только условие выше нуля должно возвращать true, остальные должны быть false. Действительно, мы можем выполнить тест с помощью другого рекурсивного метода, который используется для перебора всех элементов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
private $bound = 1000;
 
private function forallIterator($currentValue, $set, $condition) {
    if ($currentValue > $this->bound)
        return true;
    elseif ($this->contains($set, $currentValue))
        return $condition($currentValue) && $this->forallIterator($currentValue + 1, $set, $condition);
    else
        return $this->forallIterator($currentValue + 1, $set, $condition);
}
 
public function forall($set, $condition) {
    return $this->forallIterator(-$this->bound, $set, $condition);
}

Мы начнем с определения некоторых ограничений для нашего набора. Значения должны быть в диапазоне от -1000 до +1000. Это разумное ограничение, которое мы налагаем, чтобы этот пример был достаточно простым. Функция "forall" вызовет закрытый метод "forallIterator" с необходимыми параметрами, чтобы рекурсивно решить, все ли элементы соответствуют условию. В этой функции мы сначала проверяем, вышли ли мы за пределы. Если да, верните истину. Затем проверьте, содержит ли наш набор текущее значение, и верните текущее значение, примененное к условию, вместе с логическим «И» с рекурсивным вызовом себя со следующим значением. В противном случае просто вызовите себя со следующим значением и верните результат.

Это работает просто отлично, мы можем реализовать это так же, как "exists()" . Это возвращает true если любой из элементов удовлетворяет условию.

01
02
03
04
05
06
07
08
09
10
11
12
private function existsIterator($currentValue, $set, $condition) {
    if ($currentValue > $this->bound)
        return false;
    elseif ($this->contains($set, $currentValue))
        return $condition($currentValue) ||
    else
        return $this->existsIterator($currentValue + 1, $set, $condition);
}
 
public function exists($set, $condition) {
    return $this->existsIterator(-$this->bound, $set, $condition);
}

Единственное отличие состоит в том, что мы возвращаем false когда выходим за пределы, и мы используем «ИЛИ» вместо «И» во втором if.

Теперь "map()" будет другим, проще и короче.

1
2
3
4
5
6
7
public function map($set, $action) {
    return function ($currentElem) use ($set, $action) {
        return $this->exists($set, function($elem) use ($currentElem, $action) {
            return $currentElem == $action($elem);
        });
    };
}

Отображение означает, что мы применяем действие ко всем элементам набора. Для отображения нам не нужен вспомогательный итератор, мы можем повторно использовать "exists()" и возвращать те элементы «exist», которые удовлетворяют результату $action примененному к $element . Это может быть неочевидно на первом сайте, поэтому давайте посмотрим, что происходит.

  • Мы отправляем набор { 1, 2 } и действие $element * 2 (double) на карту.
  • Очевидно, он вернет функцию, которая имеет параметр в качестве элемента и использует набор и действие на один уровень выше.
  • Эта функция будет вызываться, exists с набором { 1, 2 } а функция условия $currentElement равна $elem * 2 .
  • exists() будет перебирать все элементы от -1000 до +1000, наши границы. Когда он находит элемент, двойной из того, что исходит от "contains" (значение $currentElement ), он возвращает true .
  • Другими словами, последнее сравнение вернет true для вызова to со значением два, когда значение тока, умноженное на два, приведет к двум. Таким образом, для первого элемента набора, один, он вернет true на два. Для второго элемента, два, по значению четыре.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class AuthPlugin {
 
    private $permissions = array();
 
    function authenticate($username, $password) {
        $this->verifyUser($username, $password);
 
        $adminModules = new AdminModules();
        $this->permissions[] = $adminModules->allowRead($username);
        $this->permissions[] = $adminModules->allowWrite($username);
        $this->permissions[] = $adminModules->allowExecute($username);
    }
 
    private function verifyUser($username, $password) {
        // … DO USER / PASS CHECKING
        // … LOAD USER DETAILS, ETC.
    }
 
}

Теперь это может звучать нормально, но у него огромные проблемы. 80% метода "authenticate()" использует информацию из "AdminModules" . Это создает очень сильную зависимость.

AuthFanOut

Было бы гораздо разумнее взять три вызова и создать один метод в AdminModules .

AuthReducedDep

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AuthPlugin {
 
    private $permissions = array();
    private $appModule;
 
    function __construct(ApplicationModule $appModule) {
        $this->appModule = $appModule;
    }
 
    function authenticate($username, $password) {
        $this->verifyUser($username, $password);
 
        $this->permissions = array_merge(
                $this->permissions,
                $this->appModule->getPermissions($username)
        );
    }
 
    private function verifyUser($username, $password) {
        // … DO USER / PASS CHECKING
        // … LOAD USER DETAILS, ETC.
    }
 
}

AuthPlugin получил конструктор. Он получает параметр типа ApplicationModule , интерфейс и вызывает "getPermissions()" для этого "getPermissions()" объекта.

1
2
3
4
5
interface ApplicationModule {
 
    public function getPermissions($username);
 
}

ApplicationModule определяет один публичный метод "getPermissions()" с именем пользователя в качестве параметра.

1
2
3
class AdminModules implements ApplicationModule {
    // [ … ]
}

Наконец, AdminModules должен реализовать интерфейс ApplicationModule .

AuthAdminInterface

Теперь это намного лучше. Наш AuthPlugin зависит только от интерфейса. AdminModules зависит от того же интерфейса, поэтому AuthPlugin стал независимым от модуля. Мы можем создать любое количество модулей, каждый из которых реализует ApplicationModule , и AuthPlugin сможет работать со всеми из них.

Другой способ отменить зависимость и заставить AdminModule или любой другой модуль использовать AuthPlugin — ввести в эти модули зависимость от AuthPlugin . AuthPlugin предоставит способ установить функцию аутентификации, и каждое приложение отправит свою собственную "getPermission()" .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class AdminModules {
 
    private $authPlugin;
 
    function __construct(Authentitcation $authPlugin) {
        $this->authPlugin = $authPlugin;
    }
 
    private function allowRead($username) {
        return «yes»;
    }
 
    private function allowWrite($username) {
        return «no»;
    }
 
    private function allowExecute($username) {
        return $username == «joe» ?
    }
 
    private function authenticate() {
 
        $this->authPlugin->setPermissions(
                function($username) {
                    $permissions = array();
                    $permissions[] = $this->allowRead($username);
                    $permissions[] = $this->allowWrite($username);
                    $permissions[] = $this->allowExecute($username);
                    return $permissions;
                }
        );
        $this->authPlugin->authenticate();
    }
 
}

Начнем с AdminModule . Он больше ничего не реализует. Однако он использует внедренный объект, который должен реализовать аутентификацию. В AdminModule будет метод "authenticate()" , который вызовет "setPermissions()" для AuthPlugin и передаст функцию, которую необходимо использовать.

1
2
3
4
5
6
7
interface Authentication {
 
    function setPermissions($permissionGrantingFunction);
 
    function authenticate();
 
}

Интерфейс аутентификации просто определяет два метода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class AuthPlugin implements Authentication {
 
    private $permissions = array();
    private $appModule;
    private $permissionsFunction;
 
    function __construct(ApplicationModule $appModule) {
        $this->appModule = $appModule;
    }
 
    function authenticate($username, $password) {
        $this->verifyUser($username, $password);
        $this->permissions = $this->permissionsFunction($username);
    }
 
    private function verifyUser($username, $password) {
        // … DO USER / PASS CHECKING
        // … LOAD USER DETAILS, ETC.
    }
 
    public function setPermissions($permissionGrantingFunction) {
        $this->permissionsFunction = $permissionGrantingFunction;
    }
 
}

Наконец, AuthPlugin реализует аутентификацию и устанавливает входящую функцию в атрибуте частного класса. Затем "authentication()" становится тупым методом. Он просто вызывает функцию, а затем устанавливает возвращаемое значение. Он полностью отделен от всего, что приходит.

AuthAdminCallBack

Если мы посмотрим на схему, есть два важных изменения:

  • Вместо AdminModule , AuthPlugin является тем, кто реализует интерфейс.
  • AuthPlugin «Call Back» AdminModule или любой другой модуль, отправленный в функции разрешений.

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

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