PHP: Кэширование вызовов на грани фола

в 10:50, , рубрики: php, кеширование, трюки, метки: , ,

Сидел я как-то вечером и переписывал тонны кода в рамках расширения функционала.

Код старый, кое-где костыли, но куда же без них. Бывает.

А вот на «тяжелых» методах у нас кеширование, реализованное в таком вот виде:

class GarlemShake {
...
public function getTotals($client_id) {
    static $cache = null;
    $key = md5(serialize(func_get_args()));
    if (isset($cache[$key])) {
        // вычисляем долго и упорно
        // складываем в $result
        $cache[$key] = $result;
    } else {
        $result = $cache[$key];
    }
    
    return $result;    
}
...
}

Повторяется почти по всему проекту.

Некрасиво? Да.
Долго печатать? Ну можно в сниппеты записать, и на фразу makemycache он будет выдавать заготовленный кусок кода.
Ухудшает код? Читаемость уж точно.

Но, как всем нам известно, лень — двигатель прогресса. Вот и мне пришло в голову упростить немного модель кеширования, максимально используя средства языка. За несколько минут накидал простенький прототип, складывающий результат в массив внутри себя.

class Cacheable {
        /** @var object */
	protected $_instance = null;
        /** @var array */
	protected $_cache = array();

        /**
         * Конструктор-перехватчик
         */ 
	public function __construct() {
		$params = func_get_args();
		if (!empty($params)) {		            
			$this->_instance = is_object($obj = array_shift($params)) ? $obj : new $obj($params);
		}
	}
	/**
         * Магический метод кеширования
         * 
         * @param $method
         * @param $params
         *
         * @return mixed
         */
	public function __call($method, $params) {
		$instance = isset($this->_instance) ? $this->_instance : $this;

		$key = crc32(serialize(array($method, $params)));
		if (!isset($this->cache[$key])) {
			$result = call_user_func_array(array($instance, $method), $params);
			$this->cache[$key] = $result;
		} else {
			$result = $this->cache[$key];
		}
		
		return $result;
	}
}

Из языковой магии используются магические методы __call и __construct. Первый для перехвата вызовов (далее будет рассказан финт ушами), второй для передачи объекта в ответственные руки кэширующего механизма.

Теперь пару примеров использования оборачивания вызовов

$cached_db = new Cacheable('db');
// ну или так
$db = new DB;
$cached_db = new Cacheable($db);

В этом случае все вызовы методов $cached_db будут прокешированы, чуть более, чем полностью.

А в чем же финт ушами? В процессе наследования. Предположим, у нас есть класс, некоторые функции которого нам хотелось бы закешировать

class PartialCached extends Cacheable {
    public function NotCached() {
        // ...
    }

    public function NotCachedAtAll() {
        // ...
    }
    
    protected function Cached() {
        // ...
    }
    
    private function CachedToo() {
        // ...
    }
}

Посмотрев на код, вы наверняка скажете — а где же инструкции, которые позволят указать, что кешировать, а что нет? Приглядевшись еще раз к коду, можно увидеть разницу — она в том, что при вызове protected методов наш кэширующий механизм включается, а для public кэширование отключено. И реализуется этот финт ушами средствами самого PHP, который любые не-public методы пропускает через __call, который, собственно, был перекрыт у родителя. Такие вот подарки от движка.

Как можно улучшить? Добавить парочку паттернов по вкусу (стратегии хранения кеша/хэширования ключа)

Где использовать? Решайте сами, в моем случае этот оберточный класс улучшил читаемость кода на 146%. Шучу, я еще подумаю над тем, нужно ли это проекту.

Набор для тестов и результаты:

class a {
	public function longCalc($a) {
		usleep(150000);
		return $a * $a;
	}
}

class c extends Cacheable {
	private function longCalc($a) {
		usleep(150000);
		return $a * $a;
	}
}

function microtime_float()
{
    list($usec, $sec) = explode(" ", microtime());
    return ((float)$usec + (float)$sec);
}

$b = new Cacheable('a');

$time_start = microtime_float();
/** @var $b a */
$b->longCalc(1);
echo sprintf('%.2f', microtime_float() - $time_start) . ' sec ' . PHP_EOL;

$time_start = microtime_float();
/** @var $b a */
$b->longCalc(2);
echo sprintf('%.2f', microtime_float() - $time_start) . ' sec ' . PHP_EOL;

$time_start = microtime_float();
/** @var $b a */
$b->longCalc(1);
echo sprintf('%.2f', microtime_float() - $time_start) . ' sec ' . PHP_EOL;

$c = new c;

$time_start = microtime_float();
/** @var $c c */
$c->longCalc(1);
echo sprintf('%.2f', microtime_float() - $time_start) . ' sec ' . PHP_EOL;

$time_start = microtime_float();
/** @var $c c */
$c->longCalc(2);
echo sprintf('%.2f', microtime_float() - $time_start) . ' sec ' . PHP_EOL;

$time_start = microtime_float();
/** @var $c c */
$c->longCalc(1);
echo sprintf('%.2f', microtime_float() - $time_start) . ' sec ' . PHP_EOL;

Автор: justhack

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js