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


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

Но все ли так удобно?


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

Почему практически все?


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

Как можно решить данную проблему?

   
Решение этой проблемы было изобретено давно — дополнить существующую модель ООП некоторым расширением, которое позволит описывать такие взаимодействия формально. Было проведено исследование группой инженеров Xerox PARC, в результате которого они предложили новую парадигму - аспектно-ориентированное программирование. Суть идеи проста — позволить программе взглянуть на себя «со стороны» с помощью механизма рефлексии и при надобности — провести изменение конечного кода. Имея возможность изменять конечный код, АОП получает неограниченный доступ к созданию хуков в любом месте кода и к расширению этого кода с помощью советов.
 
Для того, чтобы описать это поведение были предложены следующие термины:

1. Advice — совет. Это действие, которое нужно выполнить. Для утверждения «мыть руки перед едой» советом будет «мыть руки». Как видите, советы описывают вполне реальные действия из реального мира, а значит, могут быть представлены в виде методов, анонимных функций и замыканий. Каждый совет знает, к какому именно месту он относится, поэтому в нем доступна почти вся информация о действии.

2. Joinpoint — точка внедрения. Данный термин определяет конкретное место в программном коде, в которое может быть добавлен совет. В AspectJ, который является первоисточником АОП, доступно большое количество точек внедрения: обращение к методу, выполнение метода, инициализация класса, создание нового объекта, обращение к свойству объекта. Библиотека Go! может работать с вызовами публичных и защищенных методов, как динамических, так и статических, в том числе, и в финальных классах и трейтах; поддерживается перехват защищенных и публичных свойств у объекта. Для примера, точкой внедрения для «мыть руки перед едой» будет «начало еды», или, говоря терминами ООП — выполнение метода «кушать()» у класса «человек».

3. Pointcut — срез точек внедрения. Срез задает некоторое множество точек внедрения, в которых нужно применить совет. В мире АОП это аналог SELECT из SQL. Синтаксис для задания среза точек может быть различным, как и сама реализация, но как правило, это фильтры, которые получают на вход множество точек внедрения, из которых нужно отобрать только подходящие. В библиотеке Go! для задания срезов в основном используются аннотации, но в будущем будет поддержка xml, yaml и с помощью кода. Срез точек может выглядеть так: все публичные методы в классе, все статические методы, имеющие название *init(), все методы с определенной аннотацией и т.д.

Помимо самой сигнатуры, срез определяет еще и относительное место для внедрения совета: перед, после, вокруг точки внедрения. Для нашего случая с мытьем рук срез точек будет определять единственную точку внедрения: «перед выполнением метода человек->кушать()». Если же мы хотим добиться идеальной чистоплотности, то можем определить точку внедрения «перед выполнением методов человек->*()», что позволит нам указать наше желание всегда мыть руки перед любым действием. Однако внимательный читатель может сообразить, что метод «человек->мытьРуки()» тоже попадает под этот срез. Чем это грозит и как этого избежать — останется на самостоятельное изучение, чтобы подогреть интерес.

4. Aspect — основная единица в рамках АОП, которая позволяет собрать воедино срезы точек с теми советами, которые нужно применить. Так как наш случай относится к здоровому образу жизни, то можно назвать его HealthyLiveAspect и внести туда наш срез и несколько советов: мыть руки перед едой и чистить зубы после еды. Давайте составим список того, что мы имеем на текущий момент. Мы имеем класс «человека» с понятными методами, в них нет дополнительной логики, которая смешалась бы с основным кодом метода. Мы имеем отдельный класс аспекта с необходимыми советами. Осталось сделать самую малость — соединить их в одно целое и получить готовый код. Этот процесс называется вплетением.

5. Weaving — процесс вплетения кода советов в исходный код. Этот механизм разбирает исходный код с помощью рефлексии и применяет советы в точках внедрения. Библиотека Go! использует уникальную для PHP технологию Load-Time Weaving, которая позволяет отследить момент загрузки класса и изменить этот код до его анализа парсером PHP. Это дает возможность динамически изменять код класса, без изменений в исходном коде со стороны разработчиков. Работает это все следующим образом: в начале программы мы инициализируем ядро АОП, добавляя туда наши аспекты, после этого передаем управление основной программе. При создании объекта человека сработает автоматическая загрузка класса, которая определит нужное имя файла и попытается его загрузить. В этот момент вызов будет перехвачен ядром, далее будет выполнен статический анализ кода и проверка текущих аспектов. Ядро обнаружит, что в коде есть класс «человек» и что нужно внедрить советы в этот класс, поэтому трансформеры кода в ядре изменят оригинальное имя класса, создадут прокси-класс с оригинальным названием класса и переопределенным методом «кушать», и передадут список советов для данной точки. Дальше парсер PHP разбирает этот код и загружает его в память, при этом по имени исходного класса уже будет находиться класс-декоратор, поэтому обычный вызов метода «кушать» будет обернут в точку соединения с подключен��ыми советами.

Лучше один раз увидеть, чем сто раз услышать.


Давайте опишем все то, что мы обсудили с помощью кода. Реализуем первым делом класс человека-разумного, который будет уметь кушать, спать, работать, а также мыть руки и чистить зубы. Метод __clone пока делать не будем :)

/**
 * Human class example
 */
class Human
{

    /**
     * Eat something
     */
    public function eat()
    {
        echo "Eating...", PHP_EOL;
    }

    /**
     * Clean the teeth
     */
    public function cleanTeeth()
    {
        echo "Cleaning teeth...", PHP_EOL;
    }

    /**
     * Washing up
     */
    public function washUp()
    {
        echo "Washing up...", PHP_EOL;
    }

    /**
     * Working
     */
    public function work()
    {
        echo "Working...", PHP_EOL;
    }

    /**
     * Go to sleep
     */
    public function sleep()
    {
        echo "Go to sleep...", PHP_EOL;
    }
}


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


/**
 * Human class example
 */
class Human
{

    /**
     * Eat something
     */
    public function eat()
    {
        $this->washUp();
        echo "Eating...", PHP_EOL;
        $this->cleanTeeth();
    }

    // ....

    /**
     * Go to sleep
     */
    public function sleep()
    {
        $this->cleanTeeth();
        echo "Go to sleep...", PHP_EOL;
    }
}


Типичный программист без особых раздумий сделает так, как приведено выше: внесет вызов нужных методов в код методов «кушать» и «спать», нарушив принцип единственной ответственности каждого из этих методов. Благо, условия простые, можно разобраться с тем, почему это было сделано именно здесь. В реальной же жизни все куда печальнее: как часто вам попадается код, который делает разные вещи в одном месте и нет никакого намека на то, что этот кусок кода должен быть тут? Это и есть та самая знаменитая метрика: WTF/строчку кода. Думаю, у каждого есть такие примеры в коде ).

Следующая категория программистов ощущает подвох в том, что нужно изменять логику метода и они идут в другую крайность: делают новые методы в классе, которые объединяют в себе логику нескольких методов. А вам знакомы методы вида «мытьРукиAndКушать()»?

С одной стороны, основные методы перестанут содержать эту логику, но наш класс начнет раздуваться, появится возможность вызвать метод «кушать()» напрямую, интерфейс класса будет завален ненужными методами и количество ошибок в коде начнет ��асти. Опять не вариант.

Другие варианты я уже рассматривал в своей статье на хабре по рефакторингу с АОП, поэтому перейду сразу к АОП.

Аспектно-ориентированный подход


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

Основоположник аспектно-ориентированного программирования (АОП) Грегор Кикзалес в интервью поделился своим видением сущности АОП: "… АОП по существу – очередной этап в развитии механизмов структурирования. Сегодня понятно, что объекты не заменяют процедуры, а являются только способом создания механизма структурирования более высокого уровня. И аспекты тоже не заменяют объекты; они лишь предоставляют еще одну разновидность структурирования."

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

namespace Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;

/**
 * Healthy live aspect
 */
class HealthyLiveAspect implements Aspect
{
    /**
     * Pointcut for eat method
     *
     * @Pointcut("execution(public Human->eat(*))")
     */
    protected function humanEat() {}

    /**
     * Method that recommends to wash up before eating
     *
     * @param MethodInvocation $invocation Invocation
     * @Before(pointcut="humanEat()") // Short pointcut name
     */
    protected function washUpBeforeEat(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->washUp();
    }

    /**
     * Method that recommends to clean teeth after eating
     *
     * @param MethodInvocation $invocation Invocation
     * @After(pointcut="Aspect\HealthyLiveAspect->humanEat()") // Full-qualified pointcut name
     */
    protected function cleanTeethAfterEating(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->cleanTeeth();
    }

    /**
     * Method that recommends to clean teeth before going to sleep
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("execution(public Human->sleep())")
     */
    protected function cleanTeethBeforeSleep(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->cleanTeeth();
    }
}


Думаю, этот код достаточно понятный сам по себе, но лучше добавить некоторые комментарии.

Во-первых, мы определили срез точек — пустой метод humanEat(), помеченный с помощью специальной аннотации @Pointcut("execution(public Human->eat(*))"). В принципе, можно было не создавать отдельный срез, а указать его перед каждым советом отдельно, но так как у нас есть несколько советов для этого среза, то можно вынести его в отдельное определение. Сам метод и его код при определении среза не используются и служат лишь для указания и идентификации среза в самом аспекте.

Во-вторых, мы описали сами советы в виде методов аспекта, указав с помощью аннотации @Before и @After конкретное место внедрения для среза. Можно сразу задавать срезы в аннотации, как это сделано в методе cleanTeethBeforeSleep: @Before("execution(public Human->sleep())"). Каждый совет получает на вход нужную информацию о точке выполнения благодаря объекту MethodInvocation, содержащему в себе вызываемый объект (класс для статического метода), аргументы вызываемого метода, а также информацию о точке в программе (рефлексия метода). Аналогичная информация может быть получена и для обращения к свойствам объектов.

Теперь запустим наш код:
include isset($_GET['original']) ? './autoload.php' : './autoload_aspect.php';

// Test case with human
$man = new Human();
echo "Want to eat something, let's have a breakfast!", PHP_EOL;
$man->eat();
echo "I should work to earn some money", PHP_EOL;
$man->work();
echo "It was a nice day, go to bed", PHP_EOL;
$man->sleep();


Если мы запустим данный код в браузере, то можно будеть увидеть следующий вывод:

Want to eat something, let's have a breakfast!
Washing up...
Eating...
Cleaning teeth...
I should work to earn some money
Working...
It was a nice day, go to bed
Cleaning teeth...
Go to sleep...


Для интересующихся прочими файлами:
autoload.php
/**
 * Show all errors in code
 */
ini_set('display_errors', true);

/**
 * Register PSR-0 autoloader for our code, any components can be used here
 */
spl_autoload_register(function($originalClassName) {
    $className = ltrim($originalClassName, '\\');
    $fileName  = '';
    $namespace = '';
    if ($lastNsPos = strripos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';

    $resolvedFileName = stream_resolve_include_path($fileName);
    if ($resolvedFileName) {
        require_once $resolvedFileName;
    }
    return (bool) $resolvedFileName;
});

ob_start(function($content) {
    return str_replace(PHP_EOL, "<br>" . PHP_EOL, $content);
});

autoload_aspect.php
include '../src/Go/Core/AspectKernel.php';
include 'DemoAspectKernel.php';

// Initialize demo aspect container
DemoAspectKernel::getInstance()->init(array(
    // Configuration for autoload namespaces
    'autoload' => array(
        'Go'               => realpath(__DIR__ . '/../src'),
        'TokenReflection'  => realpath(__DIR__ . '/../vendor/andrewsville/php-token-reflection/'),
        'Doctrine\\Common' => realpath(__DIR__ . '/../vendor/doctrine/common/lib/')
    ),
    // Default application directory
    'appDir' => __DIR__ . '/../demos',
    // Cache directory for Go! generated classes
    'cacheDir' => __DIR__ . '/cache',
    // Include paths for aspect weaving
    'includePaths' => array(),
    'debug' => true
));


DemoAspectKernel.php
use Aspect\HealthyLiveAspect;

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

/**
 * Demo Aspect Kernel class
 */
class DemoAspectKernel extends AspectKernel
{

    /**
     * Returns the path to the application autoloader file, typical autoload.php
     *
     * @return string
     */
    protected function getApplicationLoaderPath()
    {
        return __DIR__ . '/autoload.php';
    }

    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     *
     * @return void
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new HealthyLiveAspect());
    }
}



Что же у нас получилось? Во-первых, логика самих методов в классе «человека» не содержит никакого мусора. Уже отлично, потому что их будет легче поддерживать. Во-вторых, нет никаких методов, дублирующих основной функционал методов класса. В-третьих, логика представлена в виде понятных советов в аспекте, потому что советы имеют реальный смысл, а это означает, что мы только что сделали декомпозицию по функциональности и сделали наше приложение более структурированным! Более того, сам класс аспекта описывает вполне понятную цель — здоровый образ жизни. Теперь все на своих местах и мы можем легко изменять как дополнительную логику, так и работать с чистой логикой самих методов.

Надеюсь, этот нехитрый пример поможет вам лучше понять концепцию аспектной парадигмы и попробовать свои силы в описании аспектной составляющей процессов в реальном мире. Для интересующихся — этот пример доступен в исходном коде библиотеки в папке demos/life.php, его можно запустить и изучить )

Ссылки:
  1. Official site http://go.aopphp.com
  2. Source code https://github.com/lisachenko/go-aop-php
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Интересна ли вам тема аспектно-ориентированного программирования?
83.3%да484
16.7%нет97
Проголосовал 581 пользователь. Воздержались 88 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Что вы думаете об АОП в PHP?
35.32%Интересная парадигма, хочу узнать больше о ней, чтобы применить в реальном проекте219
36.61%Интересно, но только теоретически, пока не буду использовать в своих проектах227
5.97%АОП уже неявно есть в проекте, использую, очень удобно (Symfony2, FLOW, прочее)37
13.39%Не буду использовать, так как это непрозрачно и непривычно для PHP83
8.71%Не понимаю, зачем вообще нужно использовать АОП и что это такое54
Проголосовали 620 пользователей. Воздержались 130 пользователей.