Архивы: по дате | по разделам | по авторам

ЕСС – продолжение

АрхивПрограммазм (архив)
автор : Юрий Кулешов   08.06.2001

Третья статья из цикла, посвященного распределенным программным системам. Продолжение разговора об Engine-Collection-Class.

Часть III. Collections

Во второй статье цикла был начат рассказ о том, что такое ECC. Для тех, кому лень ее перечитывать, конспективно повторю основные тезисы.

  1. Методика ЕСС, расшифровываемая как Engine-Collection-Class, создана для концептуального разделения подуровней «бизнес-слоя»
  2. Следование методике ЕСС позволяет быстро и эффективно разрабатывать реальные приложения «по шаблону», не тратя драгоценное время на повторяющиеся рутинные операции вроде размышлений о том, «кто должен сохранять» и «кто должен читать».
  3. Применение ECC в разработке распределённых приложений естественно и логично, поскольку изначально эта методика разрабатывалась для создания повторно используемых компонентов и сама по себе она не привязана ни к какой конкретной субплатформе Windows.
  4. Суть методики в том, что при разработке каждой новой моделируемой сущности одновременно создаются три бизнес-объекта, взаимодействующих между собой:
    1. класс Engine, создающий коллекции (Collections)
    2. класс Collection, хранящий и манипулирующий объектами класса Class
    3. класс Class, который хранит атрибуты и выполняет операции над моделируемой сущностью.

Также в предыдущей статье было показано, каким именно образом можно применить этот подход в настоящей задаче. Несколько слов о ней: поскольку до настоящего времени (пока не ушёл в отпуск) я работал над проблемами автоматизации процесса производства в цветной металлургии, то и примеры, которые будут приводиться по ходу статьи, — тоже немного… нестандартные. Сплошь и рядом будут «аноды», «анодные кожухи», «фундаменты» и «технологические параметры». Пусть это никого не смущает. Я уверен, что читателей «Софтерры» не запутать!

Итак, рассмотрим функциональность класса Collection.

Collection. Классический вариант

Свойство
Назначение
Count
Возвращает число элементов в коллекции
Item
Возвращает элемент по его индексу
_NewEnum
Обеспечивает возможность выполнения итераций вида For Each
Метод
Назначение
Add
Добавляет элемент в коллекцию. Возможна функциональность вставки (Insert)
Clear
Удаляет все элементы в коллекции, при этом последующее получение свойства Count должно вернуть 0
Delete
Удаляет один элемент из коллекции

Collection. ECC-adopted version

Свойство
Назначение
Новых свойств нет
Метод
Назначение
Update
Выполняет операции с хранилищем

Вроде всё просто, да? Но не совсем. Стандартная, описанная в историческом документе msdn.microsoft.com/library/techart/desipat.htm методика ничего не говорит относительно самих механизмов удаления, вставки и получения числа. Казалось бы, всё очевидно, однако опыт, который сын ошибок трудных, подсказывает, что не всё так просто. И в самом деле: нужно ли при удалении элемента из коллекции удалять его сразу из БД, а если так, то как организовать откат, если пользователь передумает? Каким образом сохранять данные и что должен возвращать метод Count, если мы используем механизм оптимизации, при котором данные не сразу удаляются из коллекции? Как писал в своё время замечательный детский писатель Лев Кассиль: «Наука имеет много гитик». Это и понятно. Поэтому, не претендуя на универсальность, покажу, как я реализовал механизмы, выполняющие эту работу. В общем, пройдёмся «снизу вверх»: от самой «коллекции классической» к «коллекции продвинутой».

Collectio Vulgaris и другие

Итак, «коллекция обыкновенная». Всякий знает, что коллекция — фундаментальное понятие современного программирования. VB имеет «встроенный» тип Collection. Он предоставляет почти всю необходимую нам функциональность, а именно умеет добавлять элементы, удалять их, предоставлять нам сведения о своей размерности и обеспечивать работу с циклами в стиле "For Each". Таким образом, в простейшем случае всё, что нам нужно сделать (если коллекция разрабатывается на VB) — это инкапсулировать «родную» коллекцию в свой класс и предоставить свои методы, которые работали бы с необходимыми нам классами. Это настолько элементарно, что кажется излишним демонстрировать здесь код. Тем не менее, я предлагаю его вашему вниманию (за исключением _NewEnum, но написание кода для реализации этого свойства можете рассматривать в качестве своей исследовательской работы).

Вилы

Если приглядеться, то можно увидеть, что этот код:

  1. очень сырой
  2. по-прежнему не избавляет от тех проблем, о которых говорилось выше:
    1. удаление записи или группы записей из БД происходит одновременно с вызовом метода Delete
    2. не очень ясно, каким образом поддерживать когерентность данных после того, как произошло «сохранение» данных (см. рис 1)


Рис 1. Рассогласование значений свойства Id объекта и соответствующего ключевого поля в таблице, приводящее к аварийной ситуации.

С первой проблемой-то понятно, что делать: не обращать внимания (в листинге так и сказано — «прототип»), а вот обозначенное в пункте 2 гораздо серьёзнее. Опыт промышленного применения ECC показал, что эта разумная и полезная методика не поможет, если разработчики не могут выполнить условий когерентности данных в любом из сценариев использования системы: однопользовательском или многопользовательском.

Проблемы в однопользовательском сценарии

Поясню, что это значит… (кстати, значит это не больше, чем показано на рисунке 1). Так вот, пусть пользователь добавил в коллекцию некий элемент. Значение поля Id, с которым ведётся основная технологическая работа, вообще говоря, не определено. В то же время, если после создания объекта мгновенно сохранить его в БД, то с помощью назначения полям соответствующих таблиц атрибута (Identity) можно легко получить новое значение этого самого Id. (Например, с помощью сценария Refresh, как он описан в ADO). Но каждому мало-мальски соображающему в нашем деле видно, что сразу закреплять только что созданный объект в БД бессмысленно — объекты в программах создаются миллионами, и лишь единицам их них требуется переживать перезапуски среды выполнения. Так что вариант "Urgent Persistence" не подходит. В конце-концов это будет невероятно медленно, а ресурсов потреблять будет неимоверно много. В общем, этот подход мы отчисляем из пельменной. Но проблема синхронизации-то остаётся! После того, как мы хоть раз сохранили объект в БД, появляется рассогласование ключевых полей: в конструкторе Id проинициализировано, скажем, нулём, а после сохранения SQL Server назначил соответствующему полю (которое Identity) что-то вроде 648250. Ясно, что в этом случае надо снова всё перечитывать из таблицы, но это не наш метод. Делать будем так: если при обычном сохранении объекта процедура вставки (которая spIClass) имеет вид

CREATE PROCEDURE spIClass
@non_identity_field_value sometype, -- input
@outval int OUTPUT --
AS
    INSERT INTO my_table(non_identity_field) VALUES(@descr)

    RETURN

GO


то после осознания нами проблемы, надо переписать её в следующем виде:

CREATE PROCEDURE spIClass
  @non_identity_field_value sometype,
  @outval int OUTPUT
AS
  BEGIN TRAN

    INSERT INTO my_table(non_identity_field) VALUES(@descr)
    SELECT @outval = MAX(my_table_identity_field) FROM my_table

  COMMIT TRAN

  RETURN

GO


а также исправить код метода Update

For i = 1 To Me.Count
 If Me.Item(i).Dirty Then
  If mcoll(i).IsNew Then
  ' установка всяческих параметров — см. выше
   Params.Add New ADODB.Parameter
   Params(ПОСЛЕДНИЙ_ДОБАВЛЕННЫЙ).Direction = adParamOutput
   Params(ПОСЛЕДНИЙ_ДОБАВЛЕННЫЙ).Type = adInteger
  If oDALEng.Execute(aSecurityToken, ProcName, Params) <> 0 Then
   Err.Raise ERR_FOUNDATIONINSERTFAILED, "ColFoundations::Store"
   Set oDALEng = Nothing
   Set Params = Nothing
   Exit Function
  End If

   Me.Item(i).Dirty = False
   Me.Item(i).IsNew = False
   Me.Item(i).Id = Params(ПОСЛЕДНИЙ_ДОБАВЛЕННЫЙ).Value
  Else
  ' так же см. выше
   End If
  End If

 Next


Рис 2. Исправление акогерентности через возврат значения из хранимой процедуры, ответственной за вставку объекта в БД.

А в итоге — элегантное решение синхронизации полей, значениями которых управляет БД (тот же самый код, но существенно более эффективный и реально жизненный есть и на VC++, но это не для открытой печати, уж извините).

Таким образом, проблема с когерентностью решена. Осталось определиться с удалением элемента из коллекции.

Удаление из коллекции. Дело мастера Бо

Первое, что приходит на ум — удалять данные из соответствующих таблиц, как только пользователь запросил удаление объекта из коллекции. В самом деле, этот подход отличается простотой и «дешевизной понимания», но никак не эффективностью выполнения. По той же причине, по которой разумные люди отказываются от закрепления объектов сразу при их создании, нам следует отказаться от этого сценария удаления данных.

«Но есть способ лучше!» Мы как бы удаляем данные из коллекции, не выполняя никаких операций над БД. Это даёт:

  1. выигрыш в скорости
  2. управляемость откатов в случаях, когда пользователь «передумывает»

Так ли это важно? О, да! Некоторые разработчики невероятно усложняют код для того, чтобы в их программах пользователь мог вдоволь наиграться с Alt-Backspace. В ход идут и динамически создаваемые таблицы и «файлы истории действий»; думаю, каждый из нас, немного порывшись в своей памяти, припомнит нечто подобное, что и сам некогда вытворял. Увы, в некоторых случаях overcoding'a не избежать, но откаты в изменениях данных при следовании модели ECC — не той оперы. Предлагаю использовать подход (естественно, не новый уже), при котором для элементов, подвергнутых пользователем удалению, выставляется флаг Deleted, но реально никакого удаления элемента (по крайней мере, в стиле VB Collection.Remove) не происходит. Другими словами, в случаях удаления предлагается взять на вооружение методику удаления записей горячо любимого мной FoxPro:


Рис 3. Предлагаемый механизм удаления элементов

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

Проблемы многопользовательского сценария

Этот подход, показывая свою жизнеспособность в системах с небольшим числом пользователей, способен полностью парализовать работу в большом многопользовательском приложении. В самом деле, представьте: мы оба работаем в одной системе. Я открываю справочник и удаляю, то, что необходимо удалить. Вы открываете тот же справочник и изменяете то, что я уже удалил. И ведь действительно — в сценарии с отложенным удалением данные по-прежнему хранятся в коллекции, а поскольку в соответствии с избранной стратегией данные в коллекциях всегда когерентны с данными в БД, то факт того, что данные физически не удалены из коллекции приводит к тому, что данные не удалены и из соответствующих таблиц БД. Попробуйте догадаться, что будет в БД после того, как Вы закроете изменённый справочник, а потом я закрою изменённый справочник (а потом — он, тот, этот и так сколько угодно). Другим смертельным вариантом использования системы станет случай, в котором пользователи начнут одновременно добавлять в справочники данные. Этот кошмар, известный под именем «кто последний, тот и прав» встречается в жизни разработчиков гораздо чаще, чем хотелось бы.

Задача максимально беспроблемной одновременной работы множества пользователей одной системы — одна из классических в индустриальном программировании. Организация взаимодействия объектов и уведомления их о взаимных изменениях в состояниях наверно ещё не скоро станет настолько тривиальной, чтобы можно было на ней специально не останавливаться. Сейчас же она настолько важна, что большая часть следующей статьи цикла будет посвящена исключительно применению мощи технологий Microsoft к решению этой проблемы (естественно, в применении к ECC).

Коллекции. Заключение

Как видно из приведённого выше кода, коллекция предоставляет лишь один метод для всех возможных вариантов закрепления состояния хранящихся в ней объектов. Но каким образом код метода Update определяет, что нужно делать с очередным полученным объектом? В этом, думаю, может помочь следующая таблица:

Поле IsNew
Поле IsDirty
Поле WasDoomed
Действие, которое должен выполнить метод Update
False
False
False
Ничего не делать, объект в коллекции уже сохранён
False
False
True
Удалить из хранилища
False
True
False
Выполнить код обновления через вызов процедуры семейства spUxxx
False
True
True
Удалить из хранилища
True
False
False
Выполнить код вставки через вызов процедуры семейства spIxxx
True
False
True
Просто удалить из коллекции
True
True
False
Выполнить код вставки через вызов процедуры семейства spIxxx
True
True
True
Просто удалить из коллекции

Здесь слово «семейство» означает следующее: каждая сущность, состояние которой может быть закреплено, в ECC должна быть обеспечена минимум пятью хранимыми процедурами:

spIClassName
spUClassName
spDClassName
spSClassName
spSClassNamesList


в них префикс sp значит "stored procedure", а следующая за ним буква — операцию, которая эта процедура выполняет, то есть: I = Insert, U = Update, D = Delete, S = Select. Видно, что процедур выборки минимум 2 на класс, одна из них предназначена для получения единственного элемента, а другая — для получения списка. Как они применяются будет показано в следующей статье.

И напоследок замечу, что после того, как манипуляции над элементом коллекции выполнены, необходимо сбросить все флажки его внутреннего состояния и в зависимости от количества выполненных операций удаления из БД удалить соответственное количество объектов из самой коллекции.

© ООО "Компьютерра-Онлайн", 1997-2024
При цитировании и использовании любых материалов ссылка на "Компьютерру" обязательна.