Блог


Паттерны в PHP (Registry)

Продолжим цикл статей про паттерны. Прошлая статья моя была про конструкцию global. Если кто читал, там мельком описывался паттерн Registry (реестр). Ну вот сейчас попробую описать его немного подробнее, разжевать в манную кашу, так сказать.

Вообще суть любого паттерна - упростить то, что мы до этого специально усложнили. 

 
Давайте проследим "эволюцию" зарождения патерна Registry. 

Сначала было слово... Нет, не так далеко. Сначала были машинные коды. Потом, для упрощения, появились языки программирования (ЯП). Это своего рода программы, фактически переводящие человеческую речь в те самые машинные коды. Первые ЯП были просты и примитивны, и, как следствие, программы написанные на них, были весьма и весьма громоздки. Ибо приходилось описывать каждое действие отдельно. На PHP это могло бы выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

    $var 
1;
    
    echo 
$var;
    echo 
'<br>';
    
    
$var += 1;
    
    echo 
$var;
    echo 
'<br>';
    
    
$var += 1;
    
    echo 
$var;
    echo 
'<br>';
    
    
$var += 1;
    
    echo 
$var;
    echo 
'<br>';


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php


    
function incrementing($var)
    {
        return 
$var += 1;
    }

    
$var 0;
    
    echo 
$var incrementing($var);
    echo 
'<br>';
    echo 
$var incrementing($var);
    echo 
'<br>';    
    echo 
$var incrementing($var);
    echo 
'<br>';    
    echo 
$var incrementing($var);
    echo 
'<br>';   


И всё бы ничего, но прогаммы усложнялись и требовалось большее. К примеру увеличить аргумент не на единицу, а на другое, произвольное число. Нет ничего проще, используем второй аргумент:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

    
function incrementing($var$inc)
    {
        return 
$var += $inc;
    }

    
$var 0;
    
$inc 10;
    
    echo 
$var incrementing($var$inc);
    echo 
'<br>';
    echo 
$var incrementing($var$inc);
    echo 
'<br>';    
    echo 
$var incrementing($var$inc);
    echo 
'<br>';    
    echo 
$var incrementing($var$inc);
    echo 
'<br>';  


Пока все просто. Но дело в том, что вызов функции может происходить где угодно, в разных частях скрипта и в разных файлах. И такое упрощение породило первый дискомфорт - утрату прозрачности. Натнувшись на вызов в дебрях скрипта программист начинал ломать голову - откуда взялась и что содержит переменная $inc. А так как по условиям задачи это число должно быть неизменным (ну вот такие условия), само собой напрашивалось решение передать его напрямую в функцию, не через аргумент. Для этого была придумана конструкция, позволяющая импортировать данные в функцию из глобального пространства:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

    
function incrementing($var)
    {
        global 
$inc;
     
        return 
$var += $inc;
    }

    
$var 0;
    
$inc 10;
    
    echo 
$var incrementing($var);
    echo 
'<br>';
    echo 
$var incrementing($var);
    echo 
'<br>';    
    echo 
$var incrementing($var);
    echo 
'<br>';    
    echo 
$var incrementing($var);
    echo 
'<br>'


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

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

Вот паттерн Registry и изобретался, как замена глобальным переменным. Хитрость в том, что к свойству статического класса или инициализированного объекта можно обратиться из любой точки программы. Примерно так (самый примитивный пример):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

// Самый примитивный паттерн Registry
class Registry
{   // Создаем свойство - контейнер
    
public static $container = array();
}
    
// Складываем в контейнер (регистрируем) данные
    
Registry::$container['name']    = ' Вася ';
    
Registry::$container['surname'] = ' Пупкин ';
    
    function  
getData($key)
    {
// Добраться до данных можно минуя глобальную область
       
return Registry::$container[$key];
    }
    
    echo 
getData('name');
    echo 
getData('surname');


Тут мы отчетливо видим, что в функции getData() нет конструкции global, однако мы имеем доступ к контейнеру с данными. Теперь такой доступ возможен в любой точке программы, как в глобальной области, так и в функциях и методах. По сути получился суррогат глобальной переменной.  Ну и какой в этом смысл, если по сути ничего не изменилось, элемент массива (как и весь массив) можно так же случайно или злонамеренно переопределить так же в любой точке? А правильно - никакого. Потому что так никто не делает. 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

class Registry
{   // Контейнер сделаем приватным
    
private static $container = array();
    
// Сеттер - устанавливает элемент в массив
    
public static function set($key$value)
    {
        
self::$container[$key] = $value;
    }
    
// Геттер - получает данные по ключу
    
public static function get($key)
    {
        return 
self::$container[$key];
    }    
    
}
    
// Регистрируем данные с помощью сеттера
    
Registry::set('name'' Вася ');
    
Registry::set('surname'' Пупкин ');
    
    function 
getData($key)
    {
// Получаем данные геттером
       
return Registry::get($key);
    }
    
    echo 
getData('name');
    echo 
getData('surname'); 


Вот теперь уже интереснее. Теперь контейнер невозможно ни убить ни переопределить извне класса. Однако суть проблемы осталась - все так же можно испортить отдельный элемент массива, так же как и любую глобальную переменную. Решается это простой проверкой:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

class Registry
{   
    private static 
$container = array();

    public static function 
set($key$value)
    {   
// Проверим наличие элемента в контейнере
        
if(!isset(self::$container[$key]))
            
self::$container[$key] = $value;
        else
            
trigger_error('Variable '$key .' already defined'E_USER_WARNING);
    }

    public static function 
get($key)
    {
        return 
self::$container[$key];
    }    
    
}

    
Registry::set('name'' Вася ');    
    
// Попробуем переопределить элемент
    
Registry::set('name'' Петя ');
    
    function 
getData($key)
    {
// Получаем данные геттером
       
return Registry::get($key);
    }
    
    echo 
getData('name');


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

Так что приходится выкручиваться, подставляя под хромые места подобные костыли. 

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

1
2
3
4
5
6
7
8
9
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
36
37
38
39
<?php


class Registry
{   
    private static 
$container = array();
    private static 
$lock      = array();
    
    public static function 
set($key$value)
    {   
// Проверим блокировку элемента
        
if(!isset(self::$lock[$key]))
            
self::$container[$key] = $value;
        else
            
trigger_error('Variable '$key .' is locked'E_USER_WARNING);
    }
    
// Заносим ключ в черный список
    
public static function lock($key)
    {
        
self::$lock[$key] = true;
    }    
    
// Разблокируем элемент
    
public static function unlock($key)
    {
        unset(
self::$lock[$key]);
    }
    
    public static function 
get($key)
    {
        return 
self::$container[$key];
    }    
}

    
Registry::set('name'' Вася '); 
    
// Блокируем элемент
    
Registry::lock('name');
    
// Попробуем его переопределить
    
Registry::set('name'' Петя ');
    
// Получаем данные геттером
    
echo Registry::get('name');


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php


class Registry
{   
    private static 
$container = array();
    
    public static function 
set($key$value)
    {   
// Проверим наличие элемента в контейнере
        
if(!isset(self::$container[$key]))
            
self::$container[$key] = $value;
        else
            
trigger_error('Variable '$key .' already defined'E_USER_WARNING);
    }
   
// Этот метод не проверяет наличия
    
public static function change($key$value)
    {
        
self::$container[$key] = $value;
    }   
   
    public static function 
get($key)
    {
        return 
self::$container[$key];
    }    
}

    
Registry::set('name'' Вася '); 
    
// Так можно обойти запрет
    
Registry::change('name'' Петя ');
    
// Получаем данные геттером
    
echo Registry::get('name');


Есть и другие варианты. Допустим через объект.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

class Registry 
{
    private 
$container = array();

    public function 
set($key$value
    {
        if(!isset(
$this->container[$key]))
            
$this->container[$key] = $value;
        else
            
trigger_error('Variable '$key .' already defined'E_USER_WARNING);
    }

    public function 
get($key
    {
        return 
$this->container[$key];
    }  

    
    
$RG = new Registry(); 
    
    
$RG->set('name''Вася');  
    
$RG->set('name''Петя');
    
    echo 
$RG->get('name');


Ну а коль скоро задействован объект, грех не воспользоваться магией:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
<?php

class Registry 
{
    private 
$container = array();

    public function 
__set($key$value
    {
        if(!isset(
$this->container[$key]))
            
$this->container[$key] = $value;
        else
            
trigger_error('Variable '$key .' already defined'E_USER_WARNING);
    }

    public function 
__get($key
    {
        return 
$this->container[$key];
    }  

    
    
$RG = new Registry(); 
    
    
$RG->name 'Вася';  
    
$RG->name 'Петя';
    
    echo 
$RG->name;

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

1
2
3
4
5
6
7
8
9
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
36
37
<?php

class Registry implements ArrayAccess 
{
    private 
$container = array();

    public function 
offsetSet($key$value
    {
        if(!
$this->offsetExists($key))
            
$this->container[$key] = $value;
        else
            
trigger_error('Variable '$key .' already defined'E_USER_WARNING);
    }

    public function 
offsetGet($key
    {
        return 
$this->container[$key];
    }   

    public function 
offsetExists($key)
    {
        return isset(
$this->container[$key]);
    }

    public function 
offsetUnset($key)
    {
        unset(
$this->container[$key]);
    }
}
  
    
$RG = new Registry(); 
    
    
$RG['name'] = 'Вася';  
    
$RG['name'] = 'Петя';
    
    echo 
$RG['name'];


Как говорится - сколько людей, столько и мнений.

Однако, коль скоро вы решите использовать объект, то нужно обезопаситься от нежелательной магии:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 

class Registry 
{
    private 
$container = array();
.
.
.
.
// Эти методы желательно объявить приватными
    
private function __wakeup(){}
    private function 
__clone(){}


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

Есть еще одна проблемка - сложность передачи текущего объекта реестра в потомки или зависимостью. Но это частично решается гибридом Registry и Singletone.

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

Теги: Паттерны | PHP

Комментарии (6)

Andrew
28-04-2015
Так что-то непонятно, в прошлом уроке хаяли синглтон, а в этом хвалят. По мне дак этот способ все же лучше чем глобал.
twin
28-04-2015
Тут никто ничего не хает и не хвалит. Тут просто описываются паттерны, как есть. Причем это вовсе не синглтон.

А применять или нет, и как и когда применять - на то и выбор. Сами решайте.
Andrew
28-04-2015
Ой, в голове синглтон был когда писал коммент. Я имел в виду реестр. Но все же в сложном и большом приложении глобал не так удобно использовать. Да, по большому счету можно считать, что все эти методы это лишь надстройки над глобал. Но в больших приложениях важно структурировать файлы и разделять все по пространствам, и это очень хорошо, когда есть отдельная сущность в виде класса, и понятно где он находится и лежит на ряду с другими системными классами. А глобальные переменные сложно куда-то конкретно пристроить. Чаще всего выходит так что они разбросаны по разным местам.
Striker
07-05-2015
Спасибо! Много чего нового узнал из вашего блога, продолжайте в том же духе.
Lucky
28-01-2016
Автор, очень доходчиво объяснил.
Молодец!
Денис
27-02-2016
Строчки $RG = new Registry();  и  private function __construct(){} вообщето не совместимы!
twin
05-03-2016
Действительно, ты прав. Исправил, спасибо.

 
Наверх