Шаблон: первоклассный ключ (first-class key)

Dec 17, 2011 20:39


Большому приложению, состоящему из множества модулей, бывает нужно хранить все свои настройки ( Read more... )

programming, devexperts, pattern, java

Leave a comment

elizarov December 18 2011, 17:48:40 UTC
Я не очень понял какое отношение ваша не любовь к Java-культуре имеет к этому конкретному шаблону. Шаблон первоклассного ключа отлично работает в любом объектно-ориентированным языке программирования. Поводов использовать его, например, на C++ не меньше, а то и больше, чем на Java. Более того, область его применения не ограничивается конфигурационными файлами. Мы с помощью него решаем множество разных задач.

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

> Число, строка, дата/время, путь до файла, ммм, регэксп? Мне кажется, что типы - они вне конфига уже.

Собственного, говоря, идея о том, что типов очень мало является типичным заблуждением, и это заблуждение не обошло стороной и язык Java - достаточно посмотреть на java.util.pref.Preferences. Причем, там даже видно, что авторы чуствовали, что где-то есть подвох, но не до конца понимали где он. Посмотрите на методы getXXX в Preferences - все они принимают вторым аргументом значение по умолчанию.

К сожалению, значение по умолчанию, это не единственная специфичная для конкретного ключа информация, которую необходимо знать на практике. В зависимости от конкретного приложения, может потребоваться знать диапазон допустимых значений (часто нужно для целых чисел), описание ключа (для вывода осмысленных сообщений об ошибке и подсказки) и т.п. Добавлять же всю эту информацию в аргументы в дополнение к значению по умолчанию совсем не практично. Шаблон первоклассного ключа хранит все их именно там, где им и место - в ключе. Ключ становится самостоятельной сущностью вполне заслуженно.

> Неправда, о типе значения внезапно знает также весь код, который этим ключом пользуется. На фоне этого устранение типа из метода «getXXX» - капля в море.

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

> Эта голубая мечта сгубила, ох, очень многих в общем. Кроме того, непонятно, зачем вам создавать много окон редактирования настроек.

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

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

> Да, только тут именно реестр, а не первоклассный ключ нужен.

А какие сущности, по вашему мнению, должны храниться в реестре? Собственно, один из способов придти к шаблону первоклассного ключа идет через реестр. Сначала вы делает реестр, где храните некий объект для каждого ключа, который содержит всю информацию про ключ. А потом вы вдруг понимаете, что если в этот объект еще добавить и методы get и set, то у вас будет еще больше пользы от хранящейся в нем информации.

> А про чудо-генерацию справки из кода, это тоже приятная мечта, но истории неизвестны случаи, чтобы это к чему-нибудь толковому приводило.

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

Reply

tonsky December 19 2011, 08:42:42 UTC
> Шаблон первоклассного ключа хранит все их именно там, где им и место - в ключе.

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

Конфиг - это целое, и раздербанивать его на куски непонятно зачем. Его ответственность понятна - знать, откуда загрузить конфиг, и отдать наверх уже готовое значение. Хранилище парится о том, как добыть значение - смотрит в десять конфигов по очереди, гадает, подставляет по умолчанию. А ключ? Просто делать map.get()? Вот вы пишете, что чтобы взять у ключа значение, нужно притащить туда еще и хранилище параметров. А если мы притащили хранилище параметров, зачем нам тогда, спрашивается, ключ? Почему хранилище не может отдавать значение?

Суммируя, я абсолютно согласен с идеей метаописания параметров конфига, но методов get/set у ключа быть не должно.

И чтобы оставаться конструктивным: в django есть такая штука, формы (https://docs.djangoproject.com/en/dev/ref/forms/api/#ref-forms-api-bound-unbound). Мне кажется, это подход, который идеально решает проблему пользовательских конфигов (именно логика по работе со значениями - binding, validation, cleaned_data). Кроме того, поскольку это веб-фреймворк, формы у них умеют и рисоваться - может, и вашу проблему с отрисовкой решат.

А если конфиг не пользовательский, а системный, там и java.util.Properties + getXXX хватит.

Reply

elizarov December 19 2011, 09:03:01 UTC
> как только параметры начнут зависить друг от друга, ваши ключи вдруг сразу окажутся неприменимы - обидно.

Согласен, в этом случае будет не применим. Однако, у меня есть опыт работы с реальными приложениями, где десятки независимых параметров хранятся с помощью первоклассного ключа и не разу не возникало необходимости сделать хотя бы пару параметров зависимыми. И это естественно для модульного приложениям - если модули не очень раздуты то в каждом модуле будет от силы один параметр (вообще параметры это зло и без необходимости их лучше не делать), а у разных модулей параметры очевидно независимы.

> Конфиг - это целое, и раздербанивать его на куски непонятно зачем.

Конечно, если ваше приложение монолитное и конфиг вашего приложение это нечто целое, то и шаблон первоклассного ключа вам для него не очень нужен. Вы его можете инкапсулировать в один цельный объект - код будет проще и понимать его будет проще.

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

Reply

elizarov December 19 2011, 09:35:58 UTC
> Суммируя, я абсолютно согласен с идеей метаописания параметров конфига, но методов get/set у ключа быть не должно.

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

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

Reply

tonsky December 19 2011, 09:39:26 UTC
Прочитайте про джанго формы, правда, мне к ним нечего добавить. Разве что пересказать вам про них?

Reply

elizarov December 19 2011, 15:26:06 UTC
Почитал. Там используется шаблон "связанного значения" (bound-value). Значение параметра инкапсулировано в тот же объект, где и его дополнительные параметры. Это очень популярный шаблон, который решает некоторые из задач, которые можно решить с помощью шаблона первоклассного ключа. Для тех задач, где такого шаблона вполне достаточно, может оказаться предпочтительней использовать именно его.

Опустим тот факт, что там вообще в примерах кода для джанго форм один сплошной анти-шаблон - постоянное дублирование строк с именами полей в коде. Сделаем скиду на то, что это "small-scale example" (я тоже на такие вещи в коде примеров не заморачиваюсь, а в реальном коде большого проекта отправил бы такой коммит обратно его автору во время code review даже не изучив - зачем я буду тратить свое время на посимвольное сравнения строк, если можно переписать его так, чтобы прямо из чтения кода было понятно, что там нет ошибок?).

Но это не значит, что шаблон связанного значения плох. Например, такой же шаблон используется в JOpt-simple для разбора аргументов командной строки и его можно использовать полностью type-safe и без дублирования. И на этом примере наглядно видны ограничения шаблона связанного значения. Мета-информация, будучи объединенная в общий объект с текущим значением, не может существовать в отрыве от этого значения. Конечно, мы можем единожды написать код, создающий мета-информацию по массиву значений, более того, даже можно обеспечить модульность (в каждом модуле сделав кусок кода, создающий свои объекты мета-информации). Но если значения и метаинформацию нам нужно типизированно использовать в нескольких местах кода, то тут начинаются проблемы. В JOpt-simple это и не нужно, ибо есть только одна основная операция для которой нам нужна эта мета-информация и значения - распарсить параметры строк. Да, JOpt-simple можно использовать и для генерации документации, но тут уже появляется запах - метод printHelpOn на объекте OptionParser вызывает очевидный диссонанс между классом и методом на нем. Ну а задача использовать значения разными способами перед JOpts-simple и не стоит.

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

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

Я уже не говорю о случае многих потоков. Замечательное свойство первоклассного ключа, о котором я забыл явно упомянуть (ибо оно не главное), это то, что сам по себе ключ является неизменяемым объектом и он может безопасно использоваться из разных потоков.

Reply

tonsky December 19 2011, 20:00:24 UTC
Окей, поясню.

Форма - это ровно тот самый OptionParser. Задача формы - хранить метаинформацию (.fields), грузить данные из какого-то сырого источника: веб-запрос или любой другой dict (в случае джанги через конструктор), валидировать их (.is_valid), конвертировать и отдавать словарь уже провалидированных и приведенных к нужному типу значений дальше (.cleaned_data). В том числе, она и рисовать себя умеет (справка, да?), и сообщения об ошибках генерить.

Я не очень понял, почему мета-информация и значения объединяется у вас в общий объект, когда метаинформация - это класс формы, а значения - отдельный словарь cleaned_data (объекты OptionParser/OptionSet в JOpt-simple - тоже разные вроде).

>> А как бы вы решили с помощью шаблона связанного значения

form = Form(load_data())
data = form.is_valid && form.cleaned_data
edited_data = f(data)
form = Form(edited_data)
Form.save()

Вообще, джанговские формы и этим в том числе занимаются - они хорошо объединяются с моделями, образуя ModelForm, и умеют грузиться/сохраняться в БД и одновременно отображаться/редактироваться пользователем на веб-страничке. Причем поля на веб-странице и в БД не обязательно 1:1, можно дополнительные преобразования засунуть в процесс конвертации.

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

>> Помним что приложение модульное - разные модули работают с разными значениями.

Каждому модулю свою форму? В чем проблема? Вы так модульностью пугаете, что я аж немножко нервничать начинаю, может, скоро нам всем крышка?

>> Замечательное свойство первоклассного ключа,

Замечательное свойство первоклассного ключа состоит в том, что его многопоточное использование полностью зависит от реализации хранилища, с которым он работает.

Reply

tonsky December 19 2011, 20:01:25 UTC
последняя строчка кода form.save() конечно же.

Reply

tonsky December 19 2011, 20:02:35 UTC
ну и form.is_valid and form.cleaned_data
совсем руки отвыкли

Reply

elizarov December 19 2011, 21:56:31 UTC
Огромное спасибо за конкретный пример - он намного упрощает обсуждение. Заодно я понял, что мы по-разному используем слово "метаинформация" в контексте обсуждаемой проблемы, поэтому объявляю на него бан в этой теме - чтобы не было дальнейшего недопонимания.

Давайте теперь посмотрим на первые две строчки вашего кода:

form = Form(load_data())
data = form.is_valid && form.cleaned_data
Между ними явно чего то не хватает. Каким образом метод form.is_valid узнает какие ключи имеют какие ограничения и как он сможет понять valid или не valid? Для конкретики, давайте предположим что есть ключ "timeout", которому разрешено принимать целочисленные значение от 1 до 60, и строковый ключ "mode", которому разрешено принимать одно из трех значений: "Basic", "Extended" или "Advanced". Где и как эти ограничения должны задаваться? А как указать что никаких других ключей быть не должно?

UPDATE: В доке написано что-то про класс Field. Приведите, пожалуйста, пример правильного, с вашей точки зрения, кода для этого.

Reply

tonsky December 20 2011, 08:03:07 UTC
Да, вот ссылка ссылка

Ограничения задаются в классе формы (давайте это будет MyForm, чтобы не путать с базовым forms.Form) в виде набора статических полей либо динамически в конструкторе __init__, заполняя массив fields.

class MyForm(forms.Form):
timeout = IntegerField(label='Timeout, sec', initial=30, min_value=1, max_value=60)
mode = ChoiceField (label='Some mode', required=false, choices=(('Basic', 'Basic mode'), ('Extended', 'Extended mode'), ('Advanced', 'Advanced mode'))

Reply

elizarov December 20 2011, 08:38:14 UTC
Спасибо. Очень хорошо и удобно для монолитного приложения. Давайте теперь вернемся к тебе моего поста - к "большому приложению, состоящему из множества модулей".

Предположим, что параметр timeout является конфигураций для модуля А (и вся информация он нем должна содержаться внутри модуля A), а параметр mode является конфигурацией для модуля B (и вся информация он нем должна содержаться внутри модуля B)? Модули A и B независимы друг от друга (первый ничего не знает о втором и наоборот). Как решить эту задачу с помощью шаблона связанного значения?

Reply

tonsky December 20 2011, 08:42:09 UTC

class ConfigA(forms.Form):
timeout = IntegerField(label='Timeout, sec', initial=30, min_value=1, max_value=60)

class ConfigB(forms.Form):
mode = ChoiceField (label='Some mode', required=false, choices=(('Basic', 'Basic mode'), ('Extended', 'Extended mode'), ('Advanced', 'Advanced mode'))

Reply

elizarov December 20 2011, 09:21:20 UTC
Спасибо. Пока мы движемся в правильном направлении. Уже дошли до места "более того, даже можно обеспечить модульность (в каждом модуле сделав кусок кода, создающий свои объекты мета-информации)". Собственно, вот это место, где не очень корректно употребил термин "метаинформация".

Теперь остается сделать лишь последний шаг. Представим что мы хотим повторить этот шаблон в строго-типизированном языке (C++, C#, Java и т.п.) на которых собственно и пишут большие модульные приложения.

В языке с множественным наследованием реализаций (C++) мы можем провернуть похожий трюк, унаследовав базовый класс Config от соответствующих классов модулей. Дальше придется использовать RTTI или передавать в каждое поле указатель на объект, чтобы оно там себя зарегистрировало (чтобы Config знал список своих полей). Далее, сужая Config до ConfigA и ConfigB, каждый модуль может работать со своей конфигураций типизированным образом.

В языке без множественного наследования реализаций (C#, Java) такой трюк не пройдет вообще никак. Можно, конечно, выкрутится создавая в каждом модуле конфигурационный интерфейс, указывая дополнительные параметры в аннотациях, и генерируя реализацию общего класса Config на лету (или же что-то подобное навернуть через AOP/генерацию кода и т.п.), но это уже не шаблон получается, а серъезный каркас, который не стоит усилий по его созданию ради чтения конфигурационного файла. Да и расширять такой каркас новыми типами очень не удобно: для каждого нового типа надо будет определять свою аннотацию и отдельный класс-валидатор, то есть логика размазывается по бо́льшей площади, чем в шаблоне первоклассного ключа.

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

Тут надо заметить, что я использовал похожий подход (с классом для всей конфигурации) в монолитных приложениях. Так даже удобней получается, ибо можно сразу сделать класс с нужными полями и работать с ними при чтении/записи конфигурации через рефлексию. Всё получается особенно просто, так как не нужно сильно думать о типах, которые могут быть специфичными для конкретного модуля, а просто пишется монолитный метод для работы со всеми типами.

Reply

tonsky December 20 2011, 10:12:03 UTC
Ну вот, наконец-то мы и нашли область применения вашей придумки: шаблон первоклассного ключа нужен, если у вас один ключ на модуль (никогда такого не видел, но верю, что у вас так). Окей, why not?

Про строгую типизацию, вы наверное имели в виду статическую. Не вижу проблем, почему бы не сделать то же самое в статически типизированном языке. JOpt-simple же сделали? Да даже я это делал для Фантома.

Страсти, которые вы описали, я не понял (Конфиг наследуется от классов модулей? У модулей есть классы?)

class ConfigA extends Config {
public ConfigA() {
this.fields = new Field[] {
new IntegerField('timeout'),
new ChoiseField(new String[] {'Basic', 'Advanced'})
};
}
}

Reply

elizarov December 20 2011, 19:15:49 UTC
Да, я конечно имел в виду статическую типизацию. А страсти, которые я описал, следуют из того, что я решил пропустить один этап нашего обсуждения. Я вообще-то хотел сначала спросить вас, какой вы код напишете, чтобы определить общий Config для модулей A и B так, чтобы его можно было бы централизованно загрузить, проверить на is_valid, отредактировать и т.п. И я предположил, что ответ будет примерно-таким:

class Config(ConfigA, ConfigB):

Правильно? Так можно написать? И тогда config.is_valid, например, будет правильно ведь работать?

Соответственно, сделав такое предположения я начал рассуждать про множественное наследование и всё прочее.

Что касается JOpt-simple, то он отличается от джанго форм (и от вашего примера на Фантоме) тем, что параметры в нем не являются полями объекта с точки зрения языка. Мы можем получить типизированный указатель на объект содержащий значение параметра только проделав ряд манипуляций, то есть вызвать определенные методы и, главное, надо передавать строку с именем параметра. Манипуляции можно было сократить до вызова одного метода, но имя параметра все-равно туда нужно передавать. Конечно, можно завести константу для имени каждого параметра, но это уже как раз начинает другой путь, чтобы придти в итоге к шаблону первоклассного ключа.

Соответственно, если мы хотим работать с параметрами в JOpt-simple типизированным образом более чем из одного места кода, то нам придется либо дублировать логику для доставания объекта с параметром (либо заводить методы для избежания дублирования), либо, опять же, заводить специальный класс где, a-la ваш код на Фантоме, каждое поле это объект содержащий один параметр.

Опять же, то что вы делали для Фантома можно сделать и в любом другом языке, задействовав рефлексию или RTTI. Я нечто похожее писал на Java и даже проще -- параметра типа int просто складывал в int поле с соответствующим именем (а если нужна дополнительная информация для поля, то втыкаем аннотацию). Отлично работает и даже по объему кода в чем-то сравнимо с шаблоном первоклассного ключа, но только в случаем монолитного приложения, когда параметров много и они в одном классе.

Что касается одного ключа на модуль или не одного, так тут дело только в том, какого размера у вас модули, то есть что вы считаете единицей вашего кода. Посмотрите, например, на параметры правки в MS Word:



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

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

Reply


Leave a comment

Up