В этой статье я хочу показать, как можно логгировать обращение к методам объектов и потребление памяти с помощью паттерна Декоратор.
Подробное описание паттерна, а также классическую реализацию паттерна можно посмотреть на вики.
А теперь я покажу пару примеров, как с помощью декораторов можно логгировать внешние вызовы методов класса, а также анализировать, как обращение к объекту влияет на расход оперативной памяти.
P.S. Предложенная реализация будет отличаться от классической, поскольку декоратор не будет наследован отдекорируемого класса. Это даёт как преимущества, так и определённые недостатки. Обсудим их после примера.
Примеры реализации декораторов логирования и мониторинга памяти.
Опишем абстрактный декоратор:
Создаём и декорируем хранилище статей:
В файле-логе имеем следующие результаты:
Создаём хранилище статей:
Никто не запрещал нам использовать два декоратора одновременно. Так мы и поступим:
Положительные моменты подхода:
Подробное описание паттерна, а также классическую реализацию паттерна можно посмотреть на вики.
![]() |
| UML диаграмма паттерна Декоратор |
P.S. Предложенная реализация будет отличаться от классической, поскольку декоратор не будет наследован отдекорируемого класса. Это даёт как преимущества, так и определённые недостатки. Обсудим их после примера.
Примеры реализации декораторов логирования и мониторинга памяти.
Опишем абстрактный декоратор:
/**
* Базовый декторатор объектов.
* Просто перенаправляет вывозвы методов к декорируемому объекту.
*/
abstract class ObjectDecorator
{
protected $object = null;
public function __construct($object)
{
$this->object = $object;
}
/**
* Перенаправление вызова метода
*
* @param string $methodName
* @param array $arguments
*/
public function __call($methodName, array $arguments)
{
$this->beforeMethodCalled($methodName, $arguments);
$result = call_user_func_array(array($this->object, $methodName), $arguments);
$this->afterMethodCalled($methodName, $arguments, $result);
return $result;
}
/**
* Вызывается до перенаправления вызова метода к декорируемому объекту.
*
* @param string $methodName
* @param array $arguments
*/
protected function beforeMethodCalled($methodName, array $arguments)
{}
/**
* Вызывается после перенаправления вызова метода к декорируемому объекту.
*
* @param string $methodName
* @param array $arguments
*/
protected function afterMethodCalled($methodName, array $arguments, $result)
{}
}
Теперь опишем 2 конкретных декоратора:
class Logger
{
public static function log($message)
{
//TODO: здесь надо использовать класс - логгер, но для простоты оставим так.
$f = fopen("object_log.log", "a+");
fwrite($f, date("[Y-m-d H:i:s]", time()) . " " . $message . "\n");
fclose($f);
}
}
/**
* Декоратор - логгер.
* Позволяет логгировать внешние обращения к декорируемому объекту.
*/
class LoggingDecorator extends ObjectDecorator
{
protected function beforeMethodCalled($methodName, array $arguments)
{
$message = "Method '{$methodName}' of class '"
. get_class($this->object)
. "' called with parameters "
. var_export($arguments, true);
Logger::log($message);
}
protected function afterMethodCalled($methodName, array $arguments, $result)
{
$message = "Method '{$methodName}' of class '"
. get_class($this->object)
. "' with parameters " . var_export($arguments, true)
. " returns value " . var_export($result, true);
Logger::log($message);
}
}
/**
* Декоратор - монитор использования памяти.
* Позволяет узнать, как декорируемы
* объект влияет на расход оперативной памяти.
*/
class MemoryMonitoringDecorator extends ObjectDecorator
{
protected function beforeMethodCalled($methodName, array $arguments)
{
$message = "Before method '{$methodName}' of class '"
. get_class($this->object)
. "' called: memory usage - "
. memory_get_usage() . ", memory peak usage - "
. memory_get_peak_usage();
Logger::log($message);
}
protected function afterMethodCalled($methodName, array $arguments, $result)
{
$message = "After method '{$methodName}' of class '"
. get_class($this->object)
. "' called: memory usage - "
. memory_get_usage() . ", memory peak usage - "
. memory_get_peak_usage();
Logger::log($message);
}
}
Итак, у нас есть 2 декоратора. Теперь опишем декорируемый класс, например:
/**
* Хранилище статей.
*/
class ArticleStorage
{
const ARTICLES_COUNT = 1;
/**
* Возвращает список статей сайта.
* @return array
*/
public function getArticles()
{
$articles = array();
for ($i = 0; $i < self::ARTICLES_COUNT; ++$i)
{
$articles[] = $this->prepareArticle("Article #{$i}");
}
return $articles;
}
/**
* Подготавливает и возвращает статью.
*
* @param string $articleName
* @return array
*/
private function prepareArticle($articleName)
{
$article = array(
'name' => $articleName,
//Странная генерация контента статьи =)
'content' => md5(rand(0, getrandmax())),
'comment_count' => rand(0, getrandmax()),
'author' => md5(rand(0, getrandmax()))
);
return $article;
}
}
Пример 1. Логируем обращение к публичным методам классаСоздаём и декорируем хранилище статей:
$articleStorage = new LoggingDecorator(new ArticleStorage()); $articleStorage->getArticles();
В файле-логе имеем следующие результаты:
[2011-11-22 23:42:15] Method 'getArticles' of class 'ArticleStorage' called with parameters array (
)
[2011-11-22 23:42:15] Method 'getArticles' of class 'ArticleStorage' with parameters array (
) returns value array (
0 =>
array (
'name' => 'Article #0',
'content' => '38708d88419ab3eb65a53d6e731a45f7',
'comment_count' => 15143,
'author' => '65ddc1f88e46d28edac152bdb47193e0',
),
)
Пример 2. Логируем использование памятиСоздаём хранилище статей:
$articleStorage = new MemoryMonitoringDecorator(new ArticleStorage()); $articleStorage->getArticles();В логе получаем информацию об использовании памяти:
[2011-11-23 23:19:54] Before method 'getArticles' of class 'ArticleStorage' called: memory usage - 357264, memory peak usage - 369952 [2011-11-23 23:19:54] After method 'getArticles' of class 'ArticleStorage' called: memory usage - 358024, memory peak usage - 369952Пример 3. Логируем всё сразу =)
Никто не запрещал нам использовать два декоратора одновременно. Так мы и поступим:
$articleStorage = new MemoryMonitoringDecorator( new LoggingDecorator(new ArticleStorage()) ); $articleStorage->getArticles();А результат работы этого кода можно просмотреть в логах:
[2011-11-23 23:25:41] Before method 'getArticles' of class 'LoggingDecorator' called: memory usage - 357920, memory peak usage - 369544
[2011-11-23 23:25:41] Method 'getArticles' of class 'ArticleStorage' called with parameters array (
)
[2011-11-23 23:25:41] Method 'getArticles' of class 'ArticleStorage' with parameters array (
) returns value array (
0 =>
array (
'name' => 'Article #0',
'content' => '3c8beeb8b85cec20fbe194aed2e6474c',
'comment_count' => 1886,
'author' => '0e639bc3565415e8e31989d3cfc64161',
),
)
[2011-11-23 23:25:41] After method 'getArticles' of class 'LoggingDecorator' called: memory usage - 358680, memory peak usage - 369544
ВыводыПоложительные моменты подхода:
- Получили очень интересуную возможность разрабатывать декораторы и навешивать их на объекты любого класса.
- Приведённые выше декораторы логирования памяти и вызовов методов объекта могут пригодится при дебаге и оптимизации работы скрипта по памяти. Данный подход очень напоминает Аспектно - Ориентированное программирование Аспектно - Ориентированное программирование (АОП). Возможно, этот функционал лучше реализовывать именно с помощью АОП, но это требует установки сторонних библиотек, возможно нестабильных =)
Но есть и некоторые минусы данного подхода:
- Не будет работать автокомплит в различных IDE, если создавать объекты непосредственно через new, поскольку декораторы не унаследованы от классов декорируемых объектов. Эту проблему можно обойти, если все ваши компоненты создаются, например, с помощью паттерна Абстрактная фабрика. Можно в коментариях к методу в параметре @return прописать тип возвращаемого объекта - тогда IDE будет показывать корректный автокомплит. Например вот так:
/** * @return ArticleStorage */ public function createArticlesStorage() { return new LoggingDecorator(new ArticleStorage()); } - Также, поскольку декораторы не унаследованы от классов декорируемых объектов, не будут корректно работать такие функции, как: method_exists, get_class_methods, get_class_vars и т.д.
- Если в вашей системы есть функции вида:
function do_something(ArticleStorage $storage) { //do something }то они не смогут принимать декорированный объект, опять же, поскольку декораторы не унаследованы от классов декорируемых объектов.
P.S. В следующей статье про паттерн декоратор я попытаюсь устранить последние 2 минуса подобного подхода. Возможно, генерация кода сможет помочь в данной ситуации.

Я так понимаю, эти декораторы могут использоваться только для дебага/оптимизации, верно? Ведь в продакшене они будут скорее лишними. Во всяком случае, если не будут нести, помимо логирования, какую-то полезную нагрузку.
ОтветитьУдалитьДа. Всё правильно. Приведённые два декоратора более эффективно применять при дебаге/оптимизации.
ОтветитьУдалитьНо на базе класса ObjectDecorator можно написать другие декораторы, которые будут нести полезную функциональность и для продакшен версии приложения.