Большому приложению, состоящему из множества модулей, бывает нужно хранить все свои настройки,
конфигурацию пользователя, текущий рабочий контекст, или другие свои свойства и параметры в одном месте,
чтобы централизованно всем эти управлять. Так как в модульном приложении нельзя сделать класс, который хранил
бы все параметры для всех модулей, то обычно в Java приложениях для этого используется какой-нибудь
Map. Популярен для этих целей
класс
java.util.Properties,
который используют для передачи свойств и конфигурации во множество стандартных API.
Для хранения пользовательских настроек есть специальный класс
java.util.prefs.Preferences.
Так или иначе, каждый модуль складывает туда свои параметры под своими собственными ключами,
совершенно независимо от других модулей приложения. Код, который читает параметры и общего хранилища, в общем
случае выглядит как map.get(MY_KEY), где в качестве ключей чаще всего используют простые строки.
Так как разным модулям надо хранить параметры разных типов,
да и часто нужны значения по умолчанию, то обычно выбирают одно из двух решений. Либо добавляют типизированные
методы прямо в класс хранящий настройки, как это сделано в Preferences, чтобы доставать, например, целое число, вызовом
preferences.getInt(MY_KEY, MY_DEFAULT), либо создают методы-помощники, чтобы
писать что-то типа getInt(map, MY_KEY, MY_DEFAULT).
Очевидно, что первый подход не масштабируется, в том смысле, что в большом модульном приложении будет использоваться много
разнообразных типов, и добавить метод getXXX для каждого типа XXX в общий класс хранящий параметры
не представляются практически возможным. Второй подход, с методами-помощниками, масштабируется на любое количество модулей и типов
(при условии выбора способа именования ключей исключающего их случайное дублирование), но все же, как и первый, обладает одним
серьезным недостатком. Связь между ключом и типом хранящегося в нем значения дублируется как минимум в двух местах:
там где соответствующий параметр нужно прочитать и там где его нужно записать.
Всех этих недостатков лишен шаблон, который мы называем первоклассный ключ (first-class key). В нем объект-ключ из
простого пассивного объекта превращается в полноценную сущность, а хранилище параметров становится лишь вспомогательным объектом.
Для работы, например, с целочисленными ключами нужно единожды определить класс IntKey так, чтобы для чтения
целочисленного значения писать код вида MY_KEY.get(params), где MY_KEY это конкретный экземпляр класса IntKey,
а params это это экземпляр хранилища фактических значений Parameters.
Для записи значения будет использоваться код вида MY_KEY.set(params, value).
Экземпляр первоклассного ключа (как, например, IntKey MY_KEY выше) может дополнительно содержать значение по умолчанию,
допустимый диапазон значений, подробное описание параметра и т.п., то есть инкапсулировать в себе всю возможную информацию
об этом ключе, кроме собственно значения, которое хранится отдельно. В зависимости от области применения этого шаблона,
внутренним ключом для хранилища может выступать как сам объект первоклассного ключа, так и какие-нибудь строки или скрытый
от внешнего наблюдателя объект-ключ. А значение может храниться либо в типизированным виде, либо, например, в виде строки. Методы
get и set на первоклассном ключе будут выполнять преобразование типов и все необходимые проверки
допустимости значения.
В простейшем случае, хранилищем фактических значений может выступать просто Map, Properties или
Preferences - шаблон первоклассного ключа применим к ним всем. Если же есть необходимость обезопасить
разные модули друг от друга так, чтобы параметры одного модуля не могли быть случайно или намеренно прочитаны или изменены
другим, то можно сделать специальную реализацию хранилища ключей, которая не позволит обращаться к значениям без соответствующего
экземпляра класса ключа. Это можно cделать, например, на уровне языка, объявив ограниченную видимость для методов работы со значениям
в хранилище getImpl и putImpl, а доступ к ним обеспечить через общий для всех первоклассных ключей
базовый класс AbstractKey, который расположить в том же пакете, что и класс-хранилище Parameters.
Основная задача, которую решает шаблон первоклассного ключа, это устранение дублирования информации в коде. Есть единственное
место в коде, которое определяет конкретный экземпляр каждого первоклассного ключа, что-то вроде
static final IntKey MY_KEY = new IntKey(...); и оно содержит всю информацию о его типе.
Но это не единственная задача, которая элегантно решается первоклассным ключом. Например, на основе шаблона первоклассного
ключа очень легко написать каркас для редактирования пользовательских настроек. Каждый ключ, в зависимости от его типа,
можно ассоциировать с графическим элементов для редактирования значений соответствующего типа. Тогда код для создания диалогового
окна редактирования любых настроек нужно будет написать лишь единожды. При его использовании останется только задавать список ключей,
для которых необходимо создать окно редактирования настроек, и хранилище их непосредственных значений. Конечно, такое окно
редактирования настроек будет выглядеть шаблонно, как окно пользовательских настроек в торговом приложении thinkorswim,
но зато работа по добавлению новых настроек максимально упрощается.
Став полноценным объектом, каждый ключ может инкапсулировать всё необходимую приложению логику по работе с этими ключами.
Имея реестр всех ключей для всех параметров приложения можно не только создавать создавать графические элементы для редактирования
значений, но и заполнять значения по умолчанию, преобразовать параметры в различные форматы и обратно, автоматически
подготавливать документацию и справочную информацию и т.п.
В
Devexperts мы используем этот шаблон для решения множества разных задач,
некоторые из которых имеют самостоятельную ценность и заслуживают отдельного рассказа в будущем.