AdSense - шапка

вторник, 22 ноября 2011 г.

PHP. Логгирование и мониторинг памяти с помощью паттерна Decorator(декторатор)

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

Подробное описание паттерна, а также классическую реализацию паттерна можно посмотреть на вики.
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
Выводы
Положительные моменты подхода:
  1. Получили очень интересуную возможность разрабатывать декораторы и навешивать их на объекты любого класса.
  2. Приведённые выше декораторы логирования памяти и вызовов методов объекта могут пригодится при дебаге и оптимизации работы скрипта по памяти. Данный подход очень напоминает Аспектно - Ориентированное программирование Аспектно - Ориентированное программирование (АОП). Возможно, этот функционал лучше реализовывать  именно с помощью АОП, но это требует установки сторонних библиотек, возможно нестабильных =)
Но есть и некоторые минусы данного подхода:
  1. Не будет работать автокомплит в различных IDE, если создавать объекты непосредственно через new, поскольку декораторы не унаследованы от классов декорируемых объектов. Эту проблему можно обойти, если все ваши компоненты создаются, например, с помощью паттерна Абстрактная фабрика. Можно в коментариях к методу в параметре @return прописать тип возвращаемого объекта - тогда IDE будет показывать корректный автокомплит. Например вот так:
    /**
     * @return ArticleStorage
     */
    public function createArticlesStorage()
    {
     return new LoggingDecorator(new ArticleStorage());
    }
  2. Также, поскольку декораторы не унаследованы от классов декорируемых объектов, не будут корректно работать такие функции, как: method_existsget_class_methods, get_class_vars и т.д.
  3. Если в вашей системы есть функции вида:
    function do_something(ArticleStorage $storage)
    {
     //do something
    }
    
    то они не смогут принимать декорированный объект, опять же, поскольку декораторы не унаследованы от классов декорируемых объектов.
P.S. В следующей статье про паттерн декоратор я попытаюсь устранить последние 2 минуса подобного подхода. Возможно, генерация кода сможет помочь в данной ситуации.

2 комментария:

  1. Я так понимаю, эти декораторы могут использоваться только для дебага/оптимизации, верно? Ведь в продакшене они будут скорее лишними. Во всяком случае, если не будут нести, помимо логирования, какую-то полезную нагрузку.

    ОтветитьУдалить
  2. Да. Всё правильно. Приведённые два декоратора более эффективно применять при дебаге/оптимизации.

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

    ОтветитьУдалить