Показать сообщение отдельно
Старый 02.09.2011, 00:06   #1  
gl00mie is offline
gl00mie
Участник
MCBMSS
Most Valuable Professional
Лучший по профессии 2017
Лучший по профессии 2015
Лучший по профессии 2014
Лучший по профессии AXAWARD 2013
Лучший по профессии 2011
Лучший по профессии 2009
 
3,684 / 5788 (200) ++++++++++
Регистрация: 28.11.2005
Адрес: Москва
Записей в блоге: 3
Вспомогательные классы проверки условий и утверждений
С одной стороны, хочется
  • в коде по возможности следовать контрактному программированию, чтобы ошибки не распространялись по коду и данным, а выявлялись как можно раньше;
  • выводить детализированные, четкие и понятные предупреждения и сообщения об ошибках в ходе проверки пред/постусловий и проч., чтобы и пользователи, и специалисты поддержки понимали, почему код "сломался" и/или не делает то, что ожидается, и не задавали лишних вопросов (сообщения вида "класс вызван с неверными параметрами" без дополнительных пояснений - это ни о чем);
  • следовать принципу DRY (don't repeat yourself) и не дублировать логику проверки только ради вывода сообщений об ошибках (как при этом реализовать дублирующие друг друга isXX и checkXX-методы?);
  • наконец, писать ясный, лаконичный и легко модифицируемый код.
А с другой стороны, писать в тысячный раз if (!this.Field) ret = checkFailed(strfmt("поле %1 должно быть заполнено" ... и весь сопутствующий код с ветвлениями, подчас, нет уже просто никаких сил. Из этих соображений возникла идея сделать сперва несколько методов в Global, а потом - пару специализированных классов для проверки наиболее часто встречающихся условий и утверждений, чтобы, с одной стороны, код был более компактным, понятным и надежным, а с другой, чтобы выводимые сообщения об ошибках и предупреждения были максимально информативны и при этом отключаемыми (тогда checkXX-метод легко превратится в isXX за счет дополнительного параметра). То, что получилось, - во вложении, это два класса DEV_Check и DEV_Assert, а также несколько вспомогательных модификаций общего назначения, плюс класс DEV_Desc4, задуманный изначально как универсальный "описатель" объектов вообще, но пока умеющий описывать лишь записи различных таблиц.
С ним идея была в том, чтобы в сообщениях на запись практически любой таблицы можно было ссылаться с помощью одного 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)
);
Ну и более гхм... навороченный случай - из генератора скриптов конвертации базы под AX 2009
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 );
    }
}
Вложения
Тип файла: rar DEV_Utils.rar (13.8 Кб, 315 просмотров)

Последний раз редактировалось gl00mie; 02.09.2011 в 00:21. Причина: typo...
За это сообщение автора поблагодарили: mazzy (2), AlGol (1), Logger (10), S.Kuskov (3).