Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4472.html
 

Практика #4. Обобщения и кэширование.

Опубликовано 14.05.2011 г. 16:32

Очередная статья по решению поставленной ранее задачи.

Ну поскольку взаимодействие графического интерфейса с базовым классом мы уже реализовали, то теперь нам предстоит уйти от абстракции к более конкретным реализациям. Сегодня мы будем преследовать две цели: во первых наши значения из таблице это все таки числа а не строки и во-вторых мы решили, что записываться они будут только по нажатию кнопки Сохранить, так что после того, как значения были введены, они должны храниться в каком то кэше. Это также дает нам простоту отмены изменений, понадобится всего лишь очистить этот самый кэш. Значит текущая наша задача - реализация обобщенного (generic) класса для работы с числовыми данными и поддержкой кэша. Данные мы можем представлять в виде либо целых чисел, либо с плавающей точкой. Начнем с того, как будет выглядеть наш класс. Естественно он будет унаследован от TCustomDataItem, только к этому мы добавим обобщенный параметр T. И условимся что T будет принимать значения либо integer либо double.
    TCustomDataItem<T> = class(TCustomDataItem)
      strict private
      strict protected
      public
        constructor Create();
        destructor Destroy(); override;
        procedure Cancel();   override;
    end;
Как видите я сразу привел 3 public метода. Все из них мы обсудим по порядку. Заметим что мы не можем напрямую наложить ограничения на параметр T. В том смысле, что ограничения типа integer/double. Но опять же, нам никто не мешает запретить создавать класс с некорректным параметром. Как? Проверить тип параметра при создании объекта. Проверить тип мы можем с помощью rtti, а запретить создание можем вызвав исключение в конструкторе. Но и это чуть позже. Одна из наших целей - временный кэш. Как он будет работать? Допустим мы редактируем ячейку, передаем ее в наш объект с помощью FCurrentitem.cell[] := newCellValue. И в этот момент мы не записываем данные в базу или куда либо еще, а помещаем значение в кэш. В роли кэша будет выступать обычный список на основе TList. Если мы делаем отмену изменений, то просто очищаем кэш. Если сохраняем изменения, то проходим по данному списку, и сохраняем то что там содержится. Если мы отредактировали запись, а потом отредактировали ее еще раз, то в кеше все равно будет содержаться только одно значение - последняя правка, ибо только последнее изменение имеет смысл. При обновлении таблицы данные первым делом надо искать в кэше, а только потом обращаться к нижележащему объекту. В чем отличие данных хранящихся в кэше и тех, что мы передаем из/в таблицу. Когда мы редактируем таблицу то записываем значение вида TCellValue (значение, владелец, источник). Попадая в кэш нам необходимо расширить данную информацию данными о том, в какую ячейку были внесены эти данные. При этом наш класс служит некоторым переходным мостиком между абстракцией где к ячейкам обращаются по индексу строки и столбца в таблице, и конечным классом, в котором мы сформировали заголовки и определили идентификаторы для строк и столбцов. Так что в кэше мы будем хранить все что было в TCellValue и идентификаторы строк и столбцов. А также будем конвертировать наше строковое значение в нужный нам integer или double. Что ж, введем новую структуру - TChangedValueItem, она будет параметризована. Кроме вышеперечисленных значений она будет также содержать действие, которое произошло с ячейкой - TValueAction, их мы определили в прошлый раз, и уже использовали для раскраски ячеек в таблице.
    TChangedValueItem<T> = record
        colId : integer;
        rowId  : integer;
        action : TValueAction;
        value : T;
        data : TCellValue;
        class operator Implicit(newData : TCellValue) :TChangedValueItem<T>;
    end;
Итак, идентификаторы строки и столбца (которые определены в заголовках), действие action, значение value но уже типа Т. Данные data - исходное значение TCellValue, которе содержит информацию об owner & source. А напоследок перегруженный метод Implicit - неявное приведение типа. Действительно, куда проще написать changedValue := cellValue и все. Как же будет работать это неявное приведение? Все что пришло - TCellValue мы сохраним в поле data. Данные colId,rowId,action здесь пока что не используются. Но зато мы можем сконвертировать data.value : string в T. Сделать это достаточно просто, нам понадобится вызвать либо StrToInt либо StrToFloat. Тут нам также понадобятся указатели.
class operator TChangedValueItem<T>.Implicit(newData: TCellValue): TChangedValueItem<T>;
var p : pointer;
    ival : integer;
    fval : double;
begin
    result.colId := -1;
    result.rowId := -1;
    Result.action := vaNone;
    result.data := newData;

    if length(newData.value) <> 0 then begin
        case PTypeInfo(typeInfo(T))^.Kind of
            tkInteger : begin
                            iVal := StrToInt(newData.value);
                            p := @iVal;
                        end;
            tkFloat :   begin
                            fVal := StrToFloat(newData.value);
                            p := @fVal;
                        end;
        end;
        result.value := T(p^);
    end
    else result.value := Default(T);
end;
Также конвертацию мы могли бы провести через variant. Давайте теперь опишем сам кэш - список в protected секции.
        FChangedValues : TList<TChangedValueItem<T>>;
Вообще по идее мы должны были разместить кэш в private секции, дочерним классам незачем знать о его устройстве. Тогда при сохранении значений мы бы проходили по всему кэшу и применяли бы операцию сохранения к каждому элементу последовательно. Но такой подход может быть не всегда удачен, возможно для каких то реализаций конечных классов сохранение данных целиком будет более эффективно чем поочередное. Так что оставим кэш видимым для потомков. Ну теперь мы можем подумать о конструкторе. Все что нам надо проверить здесь, это допустимость типа данных T, и создать кэш:
constructor TCustomDataItem<T>.Create();
var info : PTypeInfo;
begin
    inherited ;

    info := typeInfo(T);

    if not (info^.Kind in [tkInteger, tkFloat]) then begin
        raise Exception.Create('Используется некорректный тип данных <T> :'  + info^.Name + #13 +
                                'Допускается использование Integer или Double');
    end;

    FChangedValues  := TList<TChangedValueItem<T>>.Create();
end;
В деструкторе мы уничтожаем кэш, так что его код приводить не буду. Метод Cancel до жути прост и вызывается FChangedValues.Clear(). Вероятно при работе со значениями нам понадобится узнавать есть ли такое значение в кэше, или оно находится в исходном состоянии. Так что расширяем private секцию класса. Если значение в кэше, то мы вернем его индекс, в противном случае -1. Здесь нет ничего сложного, значение идентифицируется по ID строки и столбца.
 function TCustomDataItem<T>.getChangedValueIndex(colId, rowId: integer): integer;
var i : integer;
begin
    result := -1;

    for i := 0 to FChangedValues.Count - 1 do begin
        if (FChangedValues[i].colId = colId) and (FChangedValues[i].rowId = rowId) then begin
            exit(i);
        end;
    end;
end;
Сразу основываясь на этом мы можем легко вернуть значение состояния ячейки. Если в кеше такой нет, то состояние исходное, если же есть, то возвращаем его:
function TCustomDataItem<T>.getCellState(aCol, aRow: integer): TValueAction;
var index: integer;
    colId , rowId : integer;
begin
    colId := self.FColHeaders[aCol].id;
    rowId := self.FRowHeaders[aRow].id;

    result := vaNone;
    index := getChangedValueIndex(colId, rowId);
    if index >= 0 then
        result := FChangedValues[index].action;
end;
Эта функция перекрывает абстрактную функцию получения состояния из базового TCustomDataItem. Теперь мы немного расширим функционал нашего базового класса. Во первых добавим абстрактную функцию getValue, которая будет получать значение ячейки, но уже по идентификтору столбца/строки из заголовков. Реализация данной функции будет полностью на конечных классах. Вторая функция которая нам весьма пригодится - валидация корректности значения. Она не будет абстрактной. Тем не менее мы сделаем виртуальной и с пустой реализацией. Тем самым конечный класс не обязан будет ее реализовывать. Как проводить валидацию? По идее мы должны передать туда значение, и получить какой то ответ, успешна была проверка либо нет. Т.е вернуть какой либо код. Но с другой стороны нам было бы также неплохо в случае получить и некоторое сообщение. Поэтому условимся таким образом - функция валидации должна вызывать исключение, в случае некорректного значения. Под некорректным значением тут имеется в виду, что мы можем иметь некоторые ограничения на значения, например, значения больше нуля. Так что секция protected расширяется:
        function  getValue(coldId, rowId: integer): TCellValue; virtual; abstract;          
        procedure Validate(colId, rowId : integer; value : T); virtual;
Теперь остаются два метода для чтения и записи поля Cells - setCellValue & getCellValue. В чем будет состоять их смысл. Первый добавляет данные в кэш. Второе получает данные, при этом сначала он проверяет наличие значения в кэше:
function TCustomDataItem<T>.getCellValue(aCol, aRow: integer): TCellValue;
var ChangedIndex : integer;
    val : T;
    colId, rowId : integer;
begin
    colId := self.FColHeaders[aCol].id;
    rowId := self.FRowHeaders[aRow].id;

    changedIndex := self.getChangedValueIndex(colId,rowId);

    FillChar(result, sizeof(TCellValue), 0);

    if (ChangedIndex >= 0) then begin
        result := FChangedValues[ChangedIndex].data;
    end
    else begin
        if valueExists(colId,rowId) then
            result := getValue(colId, rowId);
    end;
end;
Ситуация с методом setCellValue немного сложнее. Опять же мы должны работать с кэшем. Но также нам требуется установить какое действие проводится с ячейкой. Если значения в текущем наборе нет, то значит происходит добавление. Если значение есть, то это либо редактирование, либо удаление. Удаление происходит, если передана пустая строка, а не число. В случае редактирования или добавления мы должны провести валидацию. И если она пройдена, то добавляем значение в кэш. Но при этом надо проверить, возможно мы исправляем ячейку повторно, тогда значение уже есть в кэше и старое надо удалить.
procedure TCustomDataItem<T>.setCellValue(aCol,aRow: integer; data : TCellValue);
var  cvItem: TChangedValueItem<T>;
    oldItemIndex : integer;
    colId,rowId  : integer;
begin
    colId := self.FColHeaders[aCol].id;
    rowId := self.FRowHeaders[aRow].id;

    cvItem := data;
    cvItem.colId := colId;
    cvItem.rowId := rowId;

    oldItemIndex := getChangedValueIndex(colId,rowId);

    if ValueExists(colId,rowId) then begin
        if data.value = '' then cvItem.action := vaDelete
        else cvItem.action := vaUpdate;
    end
    else begin
        cvItem.action := vaInsert;
        if data.value = '' then begin
            if (oldItemIndex >= 0 ) then FChangedValues.Delete(oldItemIndex);
            exit;
        end;
    end;

    if cvItem.action in [vaInsert, vaUpdate] then
        Validate(cvItem.colId, cvItem.rowId, cvItem.value);

    if oldItemIndex >= 0 then
        FChangedvalues.Delete(oldItemIndex);

    FChangedValues.Add(cvItem)
end;
Вот вроде бы и все, что касается обобщенного класса и реализации кэша данных. Заключение по данному примеру - использовали на практике обобщения (generics), настроили для них ограничения. Использовали для проверки старые методы rtti - typeinfo. Весьма неплохо опять применили переопределение неявного приведения типов. Провели валидацию основанную на исключениях. Что будет дальше? В основном все конечные классы будут получать данные из какого либо источника. При это весьма велика вероятность что источник будет в большинстве случаев одинаков. Например, это база данных, ну или xml файлы. Вот если это будет БД, то нам по сути надо только знать 4 запроса (выборка/вставка/удаление/изменение) для каждого конкретного случая. Так что функционал по созданию самих обектов запросов мы опять же можем объединить в одном родительском классе. и вероятно это будет уже заключительной частью. Содержание:
  1. Практика #1. Постановка задачи.
  2. Практика #2. Настройка таблицы.
  3. Практика #3. Абстракция
  4. Практика #4. Обобщения и кэширование.
  5. Практика #5. Конечная реализация
Метки:  ООП 

Комментарии

Нет комментариев
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно