|
02.09.2011, 00:06 | #1 |
Участник
|
Вспомогательные классы проверки условий и утверждений
С одной стороны, хочется
С ним идея была в том, чтобы в сообщениях на запись практически любой таблицы можно было ссылаться с помощью одного placeholder'а (%1), но чтобы описание при этом получалось необходимым и достаточным для идентификации записи, о которой идет речь: если это SalesTable, то чтобы был указан SalesId, если CustTable/VendTable - чтобы там был AccountNum, если CustTrans/VendTrans - чтобы обязательно были Voucher и TransDate и т.п., ну и чтобы везде был RecId, как в отладчике. И при всем при этом чтобы вызывающий код не заморачивался, описание записи какой именно таблицы он выводит. Писать обо всем этом можно много, лучше приведу несколько примеров. Проверки утверждений X++: DEV_Assert::hasTableAccess( tableBuffer.TableId, AccessType::Delete ); delete_from tableBuffer where // ... salesOriginId = DEV_Assert::returnedParmTableFieldIsNotEmpty( SalesParameters::find(), fieldnum(SalesParameters, SalesOriginId) ); // если поле не заполнено, вылетит ошибка с SysInfoAction для открытия формы, связанной с параметрической таблицей public void modifiedArrayFieldElement(ArrayIdx _idx) {; DEV_Assert::arrayIdxIsValid(_idx, dimof(this.ArrayField)); // на некорректном индексе вылетит исключение X++: protected boolean validateTransportParms() { boolean ret = DEV_Check::tableFieldNotEmpty( sysEmailParms, fieldnum(SysEmailParameters, SMTPRelayServerName) ) && DEV_Check::tableFieldValueComparesTo( sysEmailParms, fieldnum(SysEmailParameters, SMTPPortNumber), DEV_ComparisionOp::More, 0 ) && ( !mustAuthenticate || ( DEV_Check::tableFieldNotEmpty( sysEmailParms, fieldnum(SysEmailParameters, SMTPUserName) ) && DEV_Check::parameterNotEmpty( smtpPassword, fieldpname(SysEmailSMTPPassword, Password) ) ) ) ; return ret; } X++: DEV_Assert::methodIsCalledCorrectly( // если одно из условий окажется не выполненным, метод methodIsCalledCorrectly() // выведет в ошибке путь к вызвавшему его методу, взятый из стека вызовов DEV_Check::tableBufferInArgsIsSupportedAndNotEmpty( _args, tablenum(SalesTable) ) && DEV_Check::argsParmEnumTypeIs( _args, enumnum(NoYes) ) && DEV_Check::objectIs( _args.caller(), classnum(SalesFormLetter) ); X++: // устанавливает значения выражений для заполнения исходными данными конечного поля типа UtcDateTime и, // опционально, сопутствующего поля "TZID", по ходу выполняя дополнительные проверки и выводя предупреждения public void setSourceClauses4DestUtcDateTimeField( DEV_SysDestSqlDictionary _destDateTimeSqlDict, str _srcDateTimeSqlClause, fieldId _srcDateFieldId = 0, fieldId _srcTimeFieldId = 0, DEV_SysDestSqlDictionary _destTzIdSqlDict = null, str _srcTzIdSqlClause = '' ) { fieldId srcDateFieldExtId; fieldId srcTimeFieldExtId; setprefix( strfmt( @"Установка выражения для заполнения поля %1 (TZID %2)", DEV_SysDbMigrationUtil::desc4Field( _destDateTimeSqlDict ), DEV_SysDbMigrationUtil::desc4Field( _destTzIdSqlDict ) ) ); DEV_Assert::methodIsCalledCorrectly( // здесь не дублируем проверки _destDateTimeSqlDict из setSourceClause4DestinationFieldInternal() DEV_Check::tableFieldValue( _destDateTimeSqlDict, fieldnum(DEV_SysDestSqlDictionary, fieldType), #TypesUtcDateTime ) // если не указано исходное поле с датой, то и исходное поле со временем указано быть не должно && ( ( _srcDateFieldId == 0 && DEV_Check::parameterValue( _srcTimeFieldId, 0, identifierstr(_srcTimeFieldId) ) ) // если указано исходное поле с датой, то исходное поле со временем должно быть отличным от него || ( _srcDateFieldId != 0 && DEV_Check::parameterValueNot( _srcTimeFieldId, _srcDateFieldId, identifierstr(_srcTimeFieldId) ) ) ) // для системных полей createdDateTime/modifiedDateTime не должно быть поля "TZID" && ( ( isSysId( _destDateTimeSqlDict.fieldId ) && DEV_Check::tableFieldValue( _destTzIdSqlDict, fieldnum(DEV_SysDestSqlDictionary, fieldId), 0 ) && DEV_Check::parameterValue( _srcTzIdSqlClause, '', identifierstr(_srcTzIdSqlClause) ) ) // для несистемных полей типа UtcDateTime поле "TZID" должно быть обязательно указано, причем для него есть ряд доп. требований || ( !isSysId( _destDateTimeSqlDict.fieldId ) && DEV_Check::tableBufferNotEmpty( _destTzIdSqlDict ) && DEV_Check::tableFieldValue( _destTzIdSqlDict, fieldnum(DEV_SysDestSqlDictionary, tabId), _destDateTimeSqlDict.tabId ) && DEV_Check::tableFieldValue( _destTzIdSqlDict, fieldnum(DEV_SysDestSqlDictionary, fieldId), _destDateTimeSqlDict.fieldId ) && DEV_Check::tableFieldValue( _destTzIdSqlDict, fieldnum(DEV_SysDestSqlDictionary, fieldType), Types::Integer ) && DEV_Check::tableFieldValueComparesTo( _destTzIdSqlDict, fieldnum(DEV_SysDestSqlDictionary, array), DEV_ComparisionOp::More, _destDateTimeSqlDict.array ) && DEV_Check::parameterValueNot( _srcTzIdSqlClause, _srcDateTimeSqlClause, identifierstr(_srcTzIdSqlClause) ) ) ) ); this.setSourceClause4DestinationFieldInternal( _destDateTimeSqlDict, _srcDateTimeSqlClause, true ); this.markDestinationUtcDateTimeFieldAsFilled( _destDateTimeSqlDict ); this.markSourceFieldIdAsMigrated( _srcDateFieldId ); this.markSourceFieldIdAsMigrated( _srcTimeFieldId ); if (_destTzIdSqlDict) { this.setSourceClause4DestinationFieldInternal( _destTzIdSqlDict, _srcTzIdSqlClause ); } } Последний раз редактировалось gl00mie; 02.09.2011 в 00:21. Причина: typo... |
|
|
За это сообщение автора поблагодарили: mazzy (2), AlGol (1), Logger (10), S.Kuskov (3). |
02.09.2011, 08:44 | #2 |
Участник
|
Цитата:
Сообщение от gl00mie
выводить детализированные, четкие и понятные предупреждения и сообщения об ошибках в ходе проверки пред/постусловий и проч., чтобы и пользователи, и специалисты поддержки понимали, почему код "сломался" и/или не делает то, что ожидается, и не задавали лишних вопросов (сообщения вида "класс вызван с неверными параметрами" без дополнительных пояснений - это ни о чем);
Чем принципиально отличается детализированные сообщения от недетализированных? Тем что вместе с ошибкой "класс вызван с неверными параметрами" будет выведена ещё и трасировка стека? Или в сообщении "поле должно быть заполненно" будет полностью расшифрованы названия таблиц и обязательных полей? Нет пользователям не нужны такие детализированные сообщения, пользователям нужны сообщения на естественном языке, на языке бухгалтера, экономиста, кладовщика и т.д и т.п. Вот программистам нужны детализированные сообщения. Так им проще локализовать ошибку. Ставя на поток процесс обработки ошибок, есть риск совсем обезличить тексты сообщений - получить эфект машинного перевода.Есть конечно такие области системы (базовые классы, таблицы или универсальные обработки), в которых находится общая логика, где уже при всём желании невозможно конкретизировать выполняемую в данный момент задачу. Там такая автоматизация обезличенных проверок очень к стати. Задача же клиентского кода работающего с конкретной задачей пропустить минимум ошибок мимо себя в такие базовые области. Т.е. Нужно стремиться ловить ошибки как можно ближе к месту их возникновения, тогда будет проще описывать их на естественном языке. Последний раз редактировалось S.Kuskov; 02.09.2011 в 08:48. |
|
|
За это сообщение автора поблагодарили: mazzy (2). |
02.09.2011, 10:08 | #3 |
Участник
|
Цитата:
Сообщение от S.Kuskov
Чем принципиально отличается детализированные сообщения от недетализированных? Тем что вместе с ошибкой "класс вызван с неверными параметрами" будет выведена ещё и трасировка стека? Или в сообщении "поле должно быть заполненно" будет полностью расшифрованы названия таблиц и обязательных полей?
Цитата:
Цитата:
Цитата:
|
|
02.09.2011, 12:53 | #4 |
Участник
|
Цитата:
Сообщение от gl00mie
Идея в том, чтобы сделать "говорящими" по возможности все логические выражения, не перегружая при этом код, что в сочетании с грамотным использованием setprefix() подчас просто творит чудеса Без подобных средств модификации зачастую пишутся так, что проверяется с десяток самых разных условий, делается пяток ветвлений в коде, и на выходе пользователь получает лишь скупое "обновление было отменено".
P.S.: Вы не любите кошек? Да вы просто не умеете их готовить! |
|
02.09.2011, 10:20 | #5 |
Участник
|
Цитата:
Цитата:
Сообщение от gl00mie
С ним идея была в том, чтобы в сообщениях на запись практически любой таблицы можно было ссылаться с помощью одного placeholder'а (%1), но чтобы описание при этом получалось необходимым и достаточным для идентификации записи, о которой идет речь: если это SalesTable, то чтобы был указан SalesId, если CustTable/VendTable - чтобы там был AccountNum, если CustTrans/VendTrans - чтобы обязательно были Voucher и TransDate и т.п., ну и чтобы везде был RecId, как в отладчике.
при таком подходе информация об источнике возникновения выводится там, где она реально имеется. при таком подходе эту информацию не нужно спускать на нижние уровни. если юзать SysInfoAction то пользователю не нужно будет перевбивать в поиск recId - достаточно просто нажать на кнопку в инфологе. беда в том, что люди не юзают даже стандартные подходы. Даже в Майкрософте. ================= краткий совет - если не хватает информации об источнике ошибки, то всего-лишь добавьте setprefix в циклы верхнего уровня. |
|
02.09.2011, 10:54 | #6 |
Участник
|
Цитата:
Цитата:
Цитата:
На счет setprefix - целиком и полностью поддерживаю. |
|
02.09.2011, 11:45 | #7 |
Участник
|
Попробуйте setpprefix - уверен, понравится.
|
|
09.09.2011, 14:48 | #8 |
Участник
|
Цитата:
И есть вопрос, может не совсем по SUBJ. А как самому понизить уровень лога? Например, в цикле выводятся сообщения не требующие уточнения, но если будет ошибка в table.validateWrite(), то хорошо бы вывести префикс с уточнением. И, как выяснилось, оператор continue почему-то не сбрасывает уровень лога. В результате может фигня получиться. |
|
02.09.2011, 11:55 | #9 |
Участник
|
Да не надо меня за setprefix() агитировать я им уже давно и успешно пользуюсь, однако, работать приходится не только со своим собственным кодом, но и с чужим, а там setprefix() может и не использоваться вовсе.
|
|
02.09.2011, 12:13 | #10 |
Участник
|
значит - добавлять и в чужой код.
|
|
08.09.2011, 11:35 | #11 |
Участник
|
Вопрос в том, понравится ли такая модификация кода тому, кто будет поддерживать этот код после вас...
Хотя я, конечно, согласен, что в АХ не хватает методов такого плана. (Имхо, вы немного перебрали у себя с некоторыми из них, типа isMethodCalledCorrectly) Для меня лично приемлимо только если assert - команда первого уровня |
|
08.09.2011, 12:59 | #12 |
Участник
|
|
|
08.09.2011, 14:59 | #13 |
Участник
|
было бы здорово, если бы в AX 2012 создали, например, атрибуты проверки вызова метода, или тех же args(), или в методе validateField на таблице с помощью атрибутов можно было указать простейшие проверки (типа "поле должно быть заполнено", или "значение должно быть больше/меньше").
Еще до выхода 2012 такие подходы использовались в C#... @Kasperuk: Ваня, а что слышно по этому поводу "из кулуаров"?
__________________
http://www.axdevposts.blogspot.com Пришел, уведел.... отойди, дай другому увидеть! |
|
08.09.2011, 16:03 | #14 |
Участник
|
Цитата:
Понятно что хотелось бы большего. Например классно было бы помимо полей таблиц реализовать метод validate на самих расширенных типах. Тогда в него можно было бы запихнуть любые ограничения не говоря уже об ограничениях типа больше/меньше |
|
06.07.2017, 23:27 | #15 |
Участник
|
Цитата:
Сообщение от S.Kuskov
У полей таблицы есть свойство Mandatory, а у вещественных расширенных типов свойство AllowNegative
Понятно что хотелось бы большего. Например классно было бы помимо полей таблиц реализовать метод validate на самих расширенных типах. Тогда в него можно было бы запихнуть любые ограничения не говоря уже об ограничениях типа больше/меньше Тогда если для ToDate задать значением по умолчанию maxDate() то намного удобнее было бы работать с прайсами и любыми табличками где есть FromDate и ToDate Тогда ушли бы кривые условия вида X++: PriceDiscTable.fromDate <= transDate && (PriceDiscTable.ToDate >= transDate || !PriceDiscTable.ToDate) X++: PriceDiscTable.fromDate <= transDate && (PriceDiscTable.ToDate >= transDate Хотя конечно этой цели можно и сейчас достичь, просто не так удобно. |
|
08.09.2011, 15:09 | #16 |
Участник
|
О таких планах не слышал.
Аттрибуты в АХ появились только недавно, поэтому способы их применения пока не были детально рассмотреты, как я понимаю. |
|
08.09.2011, 20:22 | #17 |
Участник
|
-Ваше политическое кредо?
-Всегда! (с) "12 стульев" Свойство Mandatory у полей таблиц работает безусловно; если логика заполнения поля настолько проста, разумеется, лучше использовать метаданные и положиться на ядро, но если логика предусматривает необходимость заполнения полей по определенным условиям, тут уже Mandatory не поможет. Аналогично с AllowNegative на расширенном типе: никакие расширенные типы не позволят автоматом проверять условия вида "при одном значении поля A сумма в поле B должна быть неотрицательной, а при другом - неположительной" или "значение в поле А должно быть меньше или равно значению в поле B". Кроме того, скажем, для полей-enum'ов нельзя задать на уровне метаданных какие-либо проверки, кроме обязательности заполнения (т.е. что нельзя выбрать значение enum'а, равное нулю), в то же время в коде порой приходится проверять условия вида "статус документа должен быть меньше, чем «Накладная»" - такие проверки в любом случае надо программировать. Вообще же, дабы не раздувать дальнейшие дискуссии, открою небольшой секрет: изначально моей целью была публикация переписанного семейства классов SysExcel (см. Взаимодействие с Excel через .NET (семейство классов SysExcel), однако, эта модификация отчасти базировалась на двух других - представленных здесь классах проверки условий и утверждений и классе для преобразования значений между различными значимыми типами. Поскольку мне показалось, что эти две модификации представляют некоторую самостоятельную ценность, я опубликовал их отдельно. Это, в частности, позволит мне в дальнейшем не включать их во все публикуемые модификации, которые их используют, а просто делать ссылки в сообщении. Пользоваться ими в своей работе или же просто импортировать как довесок к семейству SysExcel (если для кого-то актуальна стабилизация работы с Excel), каждый может решить самостоятельно. Я лично пришел к выводу, что от императивных проверок лучше переходить к проверкам декларативным, в идеале - описывать нужные граничные условия с помощью атрибутов, хотя в 2009-й они еще не поддерживаются. Часть декларативных проверок реализуется свойствами расширенных типов и полей, и этим безусловно надо пользоваться, но возможности эти покрывают лишь наиболее примитивные сценарии. Инструмент для более затейливых сценариев у меня получился вот таким, возможно, для кого-то он покажется чем-то чужеродным на фоне стандартного приложения, но мне он позволяет 1) писать больше проверок в коде, 2) писать их более лаконично и наглядно, 3) получать достаточно детализированные сообщения в случае нарушения проверяемых условий без необходимости писать логику вывода сообщений во всех тех местах кода, где выполняются проверки. Да, где-то нужно "подготовить контекст" для выводимых сообщений, тот же setprefix() вызвать, но в целом текстовых строк в коде получается меньше, чем при "традиционном" подходе с кучей if и checkFailed(). Последний раз редактировалось gl00mie; 08.09.2011 в 20:27. Причина: typo |
|
08.09.2011, 22:03 | #18 |
Участник
|
За знание классики отдельное спасибо
__________________
-Ты в гномиков веришь? -Нет. -А они в тебя верят, смотри, не подведи их. |
|
10.09.2011, 12:51 | #19 |
Участник
|
В циклах setprefix отрабатывает только при первом вызове, поэтому чтобы менять префикс сообщений для каждой итерации, надо тело цикла выносить в отдельный метод и вызывать setprefix уже в нем, например, не в самом начале, а непосредственно перед table.validateWrite(), если уж так стоит задача.
|
|
|
За это сообщение автора поблагодарили: Logger (3). |
07.07.2017, 07:18 | #20 |
Участник
|
Когда мне надоело каждый раз писать обёртку try/catch со всякими assert permissions, я написал вот такой класс. В принципе - этого более чем достаточно для работы на чистом SQL. Думаю, код написан достаточно прозрачно, поясню только, что статические методы нужно использовать, если Connection и Statement не понадобятся при дальнейшем использовании. В противном случае, лучше создать экземпляр класса и использовать одно подключение, пока не закончим работать на SQL.
X++: final class PPO_SafeSQL { #define.InsufficientRights("Недостаточно прав для выполнения операции") Connection connection; Statement statement; } public void new() { ; connection = new Connection(); statement = connection.createStatement(); } public server static ResultSet runQuery(str _sql) { return new PPO_SafeSQL().executeQuery(_sql); } public server static void runUpdate(str _sql) { new PPO_SafeSQL().executeUpdate(_sql); } public ResultSet executeQuery(str _sql) { ResultSet ret; try { new SqlStatementExecutePermission(_sql).assert(); // BP deviation documented ret = statement.executeQuery(_sql); CodeAccessPermission::revertAssert(); } catch (Exception::CodeAccessSecurity) { throw error(#InsufficientRights); } return ret; } public void executeUpdate(str _sql) { try { new SqlStatementExecutePermission(_sql).assert(); // BP deviation documented statement.executeUpdate(_sql); CodeAccessPermission::revertAssert(); } catch (Exception::CodeAccessSecurity) { throw error(#InsufficientRights); } }
__________________
// no comments Последний раз редактировалось dech; 07.07.2017 в 07:27. |
|