Enum в php. Жесткая типизация перечислений



Все началось с того, что мне не понравилось что я не могу жестко типизировать входные параметры в методы моих классов, а именно типизировать константы. Из-за отсутствия enum в php (сейчас я говорю о php 5) мы сталкиваемся с неудобными вещами – мы храним данные в массивах с комментариями «не изменять», «это не трогать», но это по сути своей не правильно!

После недолгих поисков я не нашел стандартного решения своей проблемы, зато познакомился с некоторыми интересными статьями про то как люди борятся с этой проблемой. Вот эти статьи:

Все понятно и логично, но вот проблема – конечная производительность и  простота использования и расширяемость. Итак немного позаимствовав идею, расширив её и доработав я получил интересный результат. Класс реализует паттерн Одиночка (Singleton) для уменьшения расхода памяти, но конечно лишние проверки увеличивают время выполнения. Это базовый абстрактный класс Enum и исключение для него (это стоит просто скопировать и ничего не исправлять в нём):

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
final class EnumException extends Exception{}

abstract class Enum
{
    /**
     * @var array ReflectionClass
     */

    protected static $reflectorInstances = array();
    /**
     * Массив конфигурированного объекта-константы enum
     * @var array
     */

    protected static $enumInstances = array();
    /**
     * Массив соответствий значение->ключ используется для проверки -
     * если ли константа с таким значением
     * @var array
     */

    protected static $foundNameValueLink = array();
   
    protected $constName;
    protected $constValue;
   
    /**
     * Реализует паттерн "Одиночка"
     * Возвращает объект константы, но но как объект его использовать не стоит,
     * т.к. для него реализован "волшебный метод" __toString()
     * Это должно использоваться только для типизачии его как параметра
     * @paradm Node
     */

    final public static function get($value)
    {
        // Это остается здесь для увеличения производительности (по замерам ~10%)
        $name = self::getName($value);
        if ($name === false)
            throw new EnumException("Неизвестая константа");
        $className = get_called_class();   
        if (!isset(self::$enumInstances[$className][$name]))
        {
            $value = constant($className.'::'.$name);
            self::$enumInstances[$className][$name] = new $className($name, $value);
        }

        return self::$enumInstances[$className][$name];
    }
       
    /**
     * Возвращает массив констант пар ключ-значение всего перечисления
     * @return array
     */

    final public static function toArray()
    {
        $classConstantsArray = self::getReflectorInstance()->getConstants();
        foreach ($classConstantsArray as $k => $v)
            $classConstantsArray[$k] = (string)$v;
        return $classConstantsArray;
    }
   
    /**
     * Для последующего использования в toArray для получения массива констант ключ->значение
     * @return ReflectionClass
     */

    final private static function getReflectorInstance()
    {
        $className = get_called_class();
        if (!isset(self::$reflectorInstances[$className]))
        {
            self::$reflectorInstances[$className] = new ReflectionClass($className);
        }
        return self::$reflectorInstances[$className];
    }
   
    /**
     * Получает имя константы по её значению
     * @param string $value
     */

    final public static function getName($value)
    {
        $className = (string)get_called_class();
       
        $value = (string)$value;
        if (!isset(self::$foundNameValueLink[$className][$value]))
        {
            $constantName = array_search($value, self::toArray(), true);
            self::$foundNameValueLink[$className][$value] = $constantName;
        }
        return self::$foundNameValueLink[$className][$value];
    }
   
    /**
     * Используется ли такое имя константы в перечислении
     * @param string $name
     */

    final public static function isExistName($name)
    {
        $constArray = self::toArray();
        return isset($constArray[$name]);
    }
   
    /**
     * Используется ли такое значение константы в перечислении
     * @param string $value
     */

    final public static function isExistValue($value)
    {
        return self::getName($value) === false ? false : true;
    }  

   
    final private function __clone(){}

    final private function __construct($name, $value)
    {
        $this->constName = $name;
        $this->constValue = $value;
    }
   
    final public function __toString()
    {
        return (string)$this->constValue;
    }
}

Ну и теперь класс наследник. В неё нужно только задать константы и не писать никаких методов – не усложняйте код ненужными вещами – каждый класс должен быть написать для одной (абстрактной) вещи! Не нужно заставлять класс Enum еще и борщ заставлять варить. Итак класс-наследник:

1
2
3
4
5
6
7
8
/**
*Типы рабочего дня сотрудников
*/

class enumWorkType extends Enum
{
        const FULL = 0;
        const SHORT = 1;
}

Замеры быстродействия и затрат памяти

Больше ничего от от перечислений не надо. Ну и теперь замеры скорости работы и затрат памяти. Тесты довольно простые и не претендуют на полноту, но даже по ним все становится предельно ясно.

Вариант 1. Простое присваивание значений констант нашего наследованного класса enumWorkType элементам массива при 100 000 итерациях:

1
2
3
4
5
6
$a = array();
for($i = 0; $i < 100000 ;$i++)
{
    $a[] = enumWorkType::FULL;
    $a[] = enumWorkType::SHORT;
}

Запускаем в консоли:

1
2
3
4
5
6
artur@artur-desktop:$ time php common.php
Пик затрат памяти: 44800 Kb (43,75 Mb)

real    0m0.256s
user    0m0.190s
sys     0m0.060s

Неплохо. Оговорюсь, что пик замера памяти у меня считает функция такого плана:

1
2
3
4
5
6
7
8
9
function getPeakMemUsage()
{
    $message = '';
    $memUsage = memory_get_peak_usage(true);
    $message .= 'Пик затрат памяти: ';
    $message .= ($memUsage / 1024).' Kb ('.($memUsage / 1024 / 1024).' Mb)';
    $message .= "\r\n";
    return $message;       
}

Вариант 2. Присваивание элементам массива не значение констант класса, а объектов-констант типа enumWorkType. Все это делаем так же при 100 000 итерациях:

1
2
3
4
5
6
$a = array();
for($i = 0; $i < 100000 ;$i++)
{
    $a[] = enumWorkType::get(enumWorkType::FULL);
    $a[] = enumWorkType::get(enumWorkType::SHORT);
}

Запускаем и смотрим:

1
2
3
4
5
6
artur@artur-desktop:$ time php common.php
Пик затрат памяти: 29184 Kb (28,5 Mb)

real    0m1.059s
user    0m0.640s
sys     0m0.060s

Хм.. первое, что сразу бросается в глаза – заметное снижение потребления оперативной памяти аж на 35% по моим подсчетам! Но теперь обратите внимание на время выполнения – работа через класс Enum замедляет скорость в 4 раза, но это при 100 000 итерациях по 2 присваивания в каждом! Думаю в принципе это не так плохо. Далее я сделаю замеры с другим количеством итереаций. Итак:

Количество итераций Стандартное присваивание Класс Enum
500 000
1
2
3
4
5
Пик затрат памяти: 207104 Kb (202,25 Mb)

real    0m0.794s
user    0m0.580s
sys     0m0.180s
1
2
3
4
5
Пик затрат памяти: 129024 Kb (126 Mb)

real    0m2.975s
user    0m2.860s
sys     0m0.090s
1 000 000
1
2
3
4
5
Пик затрат памяти: 410880 Kb (401,25 Mb)

real    0m1.446s
user    0m1.120s
sys     0m0.320s
1
2
3
4
5
Пик затрат памяти: 254464 Kb (248,5 Mb)

real    0m6.172s
user    0m5.780s
sys     0m0.320s
5 000 000
1
2
3
4
5
Пик затрат памяти: 2088448 Kb (2039,5 Mb)

real    0m9.859s
user    0m8.280s
sys     0m1.490s
1
2
3
4
5
Пик затрат памяти: 1306880 Kb (1276,25 Mb)

real    0m29.979s
user    0m28.460s
sys     0m1.020s

По мим расчетам класс Enum экономит в этом тесте в среднем ~38% оперативной памяти, просто так потребляемой стандартным методом, но при этом увеличивает время работы на 65-75%. Но обратите внимание на объемы! Для моей задачи потребление оперативной памяти играет существенную роль, так как много данных нужно обрабатывать. В конечном итоге вам выбирать.

Плюсы class Enum

Допустим вам надо типизировать входящий параметр функции:

1
2
3
4
function setWorkTimeType($type)
{
...
}

В этом примере все неплохо, но вам потребуется валидировать этот параметр везде, где вы его используете – это увеличивает риск возникновения ошибки. А если же вы сделаете так, то интерпритатор не пропустит ничего кроме объекта типа enumWorkTime:

1
2
3
4
function setWorkTimeType(enumWorkTime $type)
{
...
}

Это, пожалуй, главный плюс использования класса Enum который позволяет избавится от лишнего кода и обеспечить 100% правильность!

1
2
3
4
5
6
7
8
9
function setWorkTimeType(enumWorkType $type){ echo 'Good'; }
$enum = enumWorkType::get(enumWorkType::SHORT);
setWorkTimeType($enum);
// Напечатает "Good"

$enum = 1;
setWorkTimeType($enum);
// Остановит программу и выдаст: PHP Catchable fatal error:  Argument 1 passed to setWorkTimeType() must be an instance of enumWorkType,
// integer given, called in /var/www/*.php on line 50 and defined in /var/www/*.php on line 45

К сожалению похожих по тематике статей пока нет.

Ответить


[ Ctrl + Enter ]