Дублирование реализации при формировании списка полей для оператора SELECT


Исходный код

  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_НАЦ' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.Percent' + R + ' AR_Percent';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_КАТ' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.ID_ArtCat' + R + ' AR_ID_ArtCat';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_ВЕС' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.Weight' + R + ' AR_Weight';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_ОБЪЕМ' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.Capacity' + R + ' AR_Capacity';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'МИННАЦ' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.MinDiscount' + R + ' AR_MinDiscount';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_МИНКОЛ' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.MinQuantity' + R + ' AR_MinQuantity';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_СТАТУС' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.Status' + R + ' AR_Status';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_ГТД' + FieldDelim, OpArtFields) > 0)) then
    sSelect := sSelect + ', ' + LMax + 'AR.GTD' + R + ' AR_GTD';
  if (TableData = 1) or ((TableData = 2) and
    (Pos(FieldDelim + 'Т_СТРАНА' + FieldDelim, OpArtFields) > 0)) then 
    sSelect := sSelect + ', ' + LMax + 'CO.Name' + R + ' AR_Country';

Что не так в этом коде

Здесь легко видеть адское дублирование реализаций:

  1. проверки наличия поля в списке требуемых полей
  2. формирования списка полей для выражения select
  3. формирования выражения для единичного поля
  4. связки «проверка на наличие поля» — «добавление поля»
  5. алгоритма выбора всех поле либо только указанных

Адское смешивание знаний о:

  1. правилах проверки наличия поля в списке требуемых полей
  2. способе формирования списка полей для выражения select
  3. способе формирования выражения для одиночного поля
  4. соответствии между кодовыми названиями полей и полями в таблицах БД
  5. полном списке всех доступных полей

Как можно улучшить исходный код

Список всех доступных полей

Самый правильный способ, как организовать список всех полей, которые должны участвовать в данном алгоритме — поместить его в константу в виде массива строк:

const
FieldCodes: array of string = ('Т_НАЦ', 'Т_КАТ', 'Т_ВЕС', 'Т_ОБЪЕМ', 'МИННАЦ', 'Т_МИНКОЛ', 'Т_СТАТУС', 'Т_ГТД', 'Т_СТРАНА');

Соответствия между кодовым названием поля и названием в БД

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

Поэтому воспользуюсь тупо функцией с ветвлением:

function FieldInfoByCode( FieldCode: string ): string
begin
  if FieldCode = 'Т_НАЦ' then Result := 'AR.Percent AR_Percent';
  elseif FieldCode = 'Т_КАТ' then Result := 'AR.ID_ArtCat AR_ID_ArtCat';
  elseif FieldCode = 'Т_ВЕС' then Result := 'AR.Weight AR_Weight';
  elseif FieldCode = 'Т_ОБЪЕМ' then Result := 'AR.Capacity AR_Capacity';
  elseif FieldCode = 'МИННАЦ' then Result := 'AR.MinDiscount AR_MinDiscount';
  elseif FieldCode = 'Т_МИНКОЛ' then Result := 'AR.MinQuantity AR_MinQuantity';
  elseif FieldCode = 'Т_СТАТУС' then Result := 'AR.Status AR_Status';
  elseif FieldCode = 'Т_ГТД' then Result := 'AR.GTD AR_GTD';
  elseif FieldCode = 'Т_СТРАНА' then Result := 'CO.Name AR_Country';
  else Result := '';
end

Отдельная функция — обязательно, так как она как раз изолирует от внешнего мира знания о соответствии названий.

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

Формирование спецификации поля

Функция FieldInfoByCode возвращает информацию о поле, которая является заготовкой. То, что попадёт в оператор SELECT, ещё нужно сформировать. Знание о том, как именно происходит формирование спецификации поля, заключаем в отдельную функцию:

function FieldSpecByInfo( FieldIndo: string, LMax: string, R: string ): string
var
  FieldInfoArr:array of string;
begin
  FieldInfoArr := SplitString(FieldInfo, ' ');
  Result := LMax + FieldInfoArr[0] + R + ' ' + FieldInfoArr[1];
end

Формирование списка полей

type
  TFieldsSpec = array of string;
function FieldsExprBySpec( FieldsSpec: TFieldsSpec ): string
begin
  Result := String.Join(', ', FieldsSpec);
end

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

Признак выбора всех доступных полей

Переменная TableData имеет предельно ни о чём не говорящее название с ни о чём не говорящими значениями. Куда более понятный вариант — завести булеву переменную с названием SelectAllFields.

Проверка необходимости включения поля

function ShouldSelectField( FieldCode: string, FieldList: string ): boolean
const 
  FieldDelim: string = ',';
begin
  Result := (Pos(FieldDelim + FieldCode + FieldDelim, FieldList) > 0);
end

Сводим всё воедино

for FieldCode in FieldCodes do
begin
  if SelectAllFields or ShouldSelectField( FieldCode, OpArtFields ) then
  begin
    FieldInfo := FieldInfoByCode( FieldCode );
    FieldSpec := FieldSpecByInfo( FieldInfo, LMax, R );

    SetLength( FieldsSpec, Length( FieldsSpec ) + 1 );
    FieldsSpec[ High( FieldsSpec ) ] := FieldSpec;
  end
  sSelect := FieldsExprBySpec( FieldsSpec );
end

Функции FieldInfoByCode и FieldSpecByInfo вызываются друг за другом, от этого возникает большой соблазн объединить их в одну функцию. Желательно этого не делать, так как они делают сильно разную работу, это два разных знания о совершенно разных вещах.

Теория