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