elizarov


Блог Романа Елизарова


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

Большому приложению, состоящему из множества модулей, бывает нужно хранить все свои настройки, конфигурацию пользователя, текущий рабочий контекст, или другие свои свойства и параметры в одном месте, чтобы централизованно всем эти управлять. Так как в модульном приложении нельзя сделать класс, который хранил бы все параметры для всех модулей, то обычно в 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 на первоклассном ключе будут выполнять преобразование типов и все необходимые проверки допустимости значения.

First-class key pattern diagram

В простейшем случае, хранилищем фактических значений может выступать просто Map, Properties или Preferences — шаблон первоклассного ключа применим к ним всем. Если же есть необходимость обезопасить разные модули друг от друга так, чтобы параметры одного модуля не могли быть случайно или намеренно прочитаны или изменены другим, то можно сделать специальную реализацию хранилища ключей, которая не позволит обращаться к значениям без соответствующего экземпляра класса ключа. Это можно cделать, например, на уровне языка, объявив ограниченную видимость для методов работы со значениям в хранилище getImpl и putImpl, а доступ к ним обеспечить через общий для всех первоклассных ключей базовый класс AbstractKey, который расположить в том же пакете, что и класс-хранилище Parameters.

Основная задача, которую решает шаблон первоклассного ключа, это устранение дублирования информации в коде. Есть единственное место в коде, которое определяет конкретный экземпляр каждого первоклассного ключа, что-то вроде static final IntKey MY_KEY = new IntKey(...); и оно содержит всю информацию о его типе.

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

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

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


  • 1
Есть другой вариант добиться того же самого. Делаем тип Key. И у класса из которых берем пропертю заводим метод T get(Key key). Внутри делаем каст к T и возвращаем.

Ага, зашибись у вас стринги в Integer или Date превратятся кастом

Если бы дело решалось кастом, то это шаблон был бы не нужен. В методе ключа может быть инкапсулирована разнообразная логика. Например, целые числа могут быть преобразованы в строки с помощью Integer.toString, а может с помощью Integer.toHexString. Даже если перобразование типов не нужно, то может потребоваться проверять допустимость значений, вставлять умолчания, посылать нотификации и т.п - у этого шаблона очень широкий круг применения.

Спасибо, Роман! Это просто отличный пример, за что я не люблю Джава-культуру.

Вы придумали себе проблему и успешно ее решили. Вы сделали из простой вещи сложную. Вы наплодили сущностей. Вы нагородили иерархию классов и даже UML-диаграмму нарисовали!

Чтобы мои нападки не выглядели голословными (извините, если чересчур резко), прокомментирую:

> в большом модульном приложении будет использоваться много разнообразных типов

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

> Есть единственное место в коде, которое определяет конкретный экземпляр каждого первоклассного ключа, что-то вроде static final IntKey MY_KEY = new IntKey(...); и оно содержит всю информацию о его типе.

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

> Тогда код для создания диалогового окна редактирования любых настроек нужно будет написать лишь единожды

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

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

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

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

Еще раз, ни в коем случае не хотел вас обидеть, только донести мысль.

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Edited at 2011-12-18 05:51 pm (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 хватит.

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

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

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

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

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


Edited at 2011-12-19 09:31 am (UTC)

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

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

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

Прочитайте про джанго формы, правда, мне к ним нечего добавить. Разве что пересказать вам про них?

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

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

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

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

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

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

Окей, поясню.

Форма — это ровно тот самый 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, можно дополнительные преобразования засунуть в процесс конвертации.

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

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

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

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

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

последняя строчка кода form.save() конечно же.

ну и form.is_valid and form.cleaned_data
совсем руки отвыкли

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

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

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. Приведите, пожалуйста, пример правильного, с вашей точки зрения, кода для этого.


Edited at 2011-12-19 10:02 pm (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'))

Спасибо. Очень хорошо и удобно для монолитного приложения. Давайте теперь вернемся к тебе моего поста — к "большому приложению, состоящему из множества модулей".

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

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'))

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

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

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

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

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

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

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

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

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

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

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

class Config(ConfigA, ConfigB):

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

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

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

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

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

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

word edit params

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

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

>> общий Config для модулей A и B так, чтобы его можно было бы централизованно загрузить, проверить на is_valid, отредактировать и т.п.

Properties config = new Properties().load(file);
...
ConfigA a = new ConfigA(config);
...
ConfigB b = new ConfigB(config);


>> Нужно ли всем этим методам зависеть от какого-то общего Config класса, который знает что-то про каждый из них?

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

>> главное, надо передавать строку с именем параметра

Не вижу разницы между тем, что передавать — строку или константу с именем строки. Но сколько вы смогли высосать из этой детали — впечатляет.

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

То есть вас всё устраивало и проблем не было, но вы от этого простого решения ушли, руководствуясь просто абстрактным принципом?

Ну Ok. Тоже вариант.

>Не вижу разницы между тем, что передавать — строку или константу с именем строки. Но сколько вы смогли высосать из этой детали — впечатляет.

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

>То есть вас всё устраивало и проблем не было, но вы от этого простого решения ушли, руководствуясь просто абстрактным принципом?

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

Это банально позволяет сэкономить в объеме кода (воздержимся пока от применения более субъективных метрик кода). Какждый экземпляр первоклассного ключа в модуле это одна строка, а его чтения или запись внутри модуля концептуально не тяжелей чтения/записи поля в классе конфигурации: MODE.get(props) вместо config.getMode().


Edited at 2011-12-21 10:54 am (UTC)

>> человек может легко убедиться в правильности этого кода при его прочтении

Увы, убедиться он не может, но думать, что убедился — да. Мне не нужно объяснять разницу — было время, я тоже думал как вы, и все аргументы знаю.

Не будут ни в чем убеждать. Для меня возможность думать, что я убедился в правильности кода, вполне достаточна. Зачем именно она мне нужна — раскажу отдельно.

А как сделать правильно и/или на пост-индустриальных языках?

Мне кажется, джанго формы идеально ложатся на проблему (см. коммент выше)

  • 1
?

Log in

No account? Create an account