ЕСС – продолжение
АрхивПрограммазм (архив)Третья статья из цикла, посвященного распределенным программным системам. Продолжение разговора об Engine-Collection-Class.
Часть III. Collections
Во второй статье цикла был начат рассказ о том, что такое ECC. Для тех, кому лень ее перечитывать, конспективно повторю основные тезисы.
- Методика ЕСС, расшифровываемая как Engine-Collection-Class, создана для концептуального разделения подуровней «бизнес-слоя»
- Следование методике ЕСС позволяет быстро и эффективно разрабатывать реальные приложения «по шаблону», не тратя драгоценное время на повторяющиеся рутинные операции вроде размышлений о том, «кто должен сохранять» и «кто должен читать».
- Применение ECC в разработке распределённых приложений естественно и логично, поскольку изначально эта методика разрабатывалась для создания повторно используемых компонентов и сама по себе она не привязана ни к какой конкретной субплатформе Windows.
- Суть методики в том, что при разработке каждой новой моделируемой сущности одновременно создаются три бизнес-объекта, взаимодействующих между собой:
- класс Engine, создающий коллекции (Collections)
- класс Collection, хранящий и манипулирующий объектами класса Class
- класс Class, который хранит атрибуты и выполняет операции над моделируемой сущностью.
Также в предыдущей статье было показано, каким именно образом можно применить этот подход в настоящей задаче. Несколько слов о ней: поскольку до настоящего времени (пока не ушёл в отпуск) я работал над проблемами автоматизации процесса производства в цветной металлургии, то и примеры, которые будут приводиться по ходу статьи, — тоже немного… нестандартные. Сплошь и рядом будут «аноды», «анодные кожухи», «фундаменты» и «технологические параметры». Пусть это никого не смущает. Я уверен, что читателей «Софтерры» не запутать!
Итак, рассмотрим функциональность класса Collection.
Collection. Классический вариант
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Collection. ECC-adopted version
|
|
Новых свойств нет | |
|
|
|
|
Вроде всё просто, да? Но не совсем. Стандартная, описанная в историческом документе msdn.microsoft.com/library/techart/desipat.htm методика ничего не говорит относительно самих механизмов удаления, вставки и получения числа. Казалось бы, всё очевидно, однако опыт, который сын ошибок трудных, подсказывает, что не всё так просто. И в самом деле: нужно ли при удалении элемента из коллекции удалять его сразу из БД, а если так, то как организовать откат, если пользователь передумает? Каким образом сохранять данные и что должен возвращать метод Count, если мы используем механизм оптимизации, при котором данные не сразу удаляются из коллекции? Как писал в своё время замечательный детский писатель Лев Кассиль: «Наука имеет много гитик». Это и понятно. Поэтому, не претендуя на универсальность, покажу, как я реализовал механизмы, выполняющие эту работу. В общем, пройдёмся «снизу вверх»: от самой «коллекции классической» к «коллекции продвинутой».
Collectio Vulgaris и другие
Итак, «коллекция обыкновенная». Всякий знает, что коллекция — фундаментальное понятие современного программирования. VB имеет «встроенный» тип Collection. Он предоставляет почти всю необходимую нам функциональность, а именно умеет добавлять элементы, удалять их, предоставлять нам сведения о своей размерности и обеспечивать работу с циклами в стиле "For Each". Таким образом, в простейшем случае всё, что нам нужно сделать (если коллекция разрабатывается на VB) — это инкапсулировать «родную» коллекцию в свой класс и предоставить свои методы, которые работали бы с необходимыми нам классами. Это настолько элементарно, что кажется излишним демонстрировать здесь код. Тем не менее, я предлагаю его вашему вниманию (за исключением _NewEnum, но написание кода для реализации этого свойства можете рассматривать в качестве своей исследовательской работы).
Вилы
Если приглядеться, то можно увидеть, что этот код:
- очень сырой
- по-прежнему не избавляет от тех проблем, о которых говорилось выше:
- удаление записи или группы записей из БД происходит одновременно с вызовом метода Delete
- не очень ясно, каким образом поддерживать когерентность данных после того, как произошло «сохранение» данных (см. рис 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++, но это не для открытой печати, уж извините).
Таким образом, проблема с когерентностью решена. Осталось определиться с удалением элемента из коллекции.
Удаление из коллекции. Дело мастера Бо
Первое, что приходит на ум — удалять данные из соответствующих таблиц, как только пользователь запросил удаление объекта из коллекции. В самом деле, этот подход отличается простотой и «дешевизной понимания», но никак не эффективностью выполнения. По той же причине, по которой разумные люди отказываются от закрепления объектов сразу при их создании, нам следует отказаться от этого сценария удаления данных.
«Но есть способ лучше!» Мы как бы удаляем данные из коллекции, не выполняя никаких операций над БД. Это даёт:
- выигрыш в скорости
- управляемость откатов в случаях, когда пользователь «передумывает»
Так ли это важно? О, да! Некоторые разработчики невероятно усложняют код для того, чтобы в их программах пользователь мог вдоволь наиграться с Alt-Backspace. В ход идут и динамически создаваемые таблицы и «файлы истории действий»; думаю, каждый из нас, немного порывшись в своей памяти, припомнит нечто подобное, что и сам некогда вытворял. Увы, в некоторых случаях overcoding'a не избежать, но откаты в изменениях данных при следовании модели ECC — не той оперы. Предлагаю использовать подход (естественно, не новый уже), при котором для элементов, подвергнутых пользователем удалению, выставляется флаг Deleted, но реально никакого удаления элемента (по крайней мере, в стиле VB Collection.Remove) не происходит. Другими словами, в случаях удаления предлагается взять на вооружение методику удаления записей горячо любимого мной FoxPro:
Рис 3. Предлагаемый механизм удаления элементов
Полагая, что никаких особых пояснений по рисунку не нужно, скажу лишь, что во второй фазе данные (элементы в коллекции) могут находиться потенциально бесконечно. То есть, их для пользователя уже нет, но они хранятся в коллекции.
Проблемы многопользовательского сценария
Этот подход, показывая свою жизнеспособность в системах с небольшим числом пользователей, способен полностью парализовать работу в большом многопользовательском приложении. В самом деле, представьте: мы оба работаем в одной системе. Я открываю справочник и удаляю, то, что необходимо удалить. Вы открываете тот же справочник и изменяете то, что я уже удалил. И ведь действительно — в сценарии с отложенным удалением данные по-прежнему хранятся в коллекции, а поскольку в соответствии с избранной стратегией данные в коллекциях всегда когерентны с данными в БД, то факт того, что данные физически не удалены из коллекции приводит к тому, что данные не удалены и из соответствующих таблиц БД. Попробуйте догадаться, что будет в БД после того, как Вы закроете изменённый справочник, а потом я закрою изменённый справочник (а потом — он, тот, этот и так сколько угодно). Другим смертельным вариантом использования системы станет случай, в котором пользователи начнут одновременно добавлять в справочники данные. Этот кошмар, известный под именем «кто последний, тот и прав» встречается в жизни разработчиков гораздо чаще, чем хотелось бы.
Задача максимально беспроблемной одновременной работы множества пользователей одной системы — одна из классических в индустриальном программировании. Организация взаимодействия объектов и уведомления их о взаимных изменениях в состояниях наверно ещё не скоро станет настолько тривиальной, чтобы можно было на ней специально не останавливаться. Сейчас же она настолько важна, что большая часть следующей статьи цикла будет посвящена исключительно применению мощи технологий Microsoft к решению этой проблемы (естественно, в применении к ECC).
Коллекции. Заключение
Как видно из приведённого выше кода, коллекция предоставляет лишь один метод для всех возможных вариантов закрепления состояния хранящихся в ней объектов. Но каким образом код метода Update определяет, что нужно делать с очередным полученным объектом? В этом, думаю, может помочь следующая таблица:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Здесь слово «семейство» означает следующее: каждая сущность, состояние которой может быть закреплено, в ECC должна быть обеспечена минимум пятью хранимыми процедурами:
spIClassName
spUClassName
spDClassName
spSClassName
spSClassNamesList
в них префикс sp значит "stored procedure", а следующая за ним буква — операцию, которая эта процедура выполняет, то есть: I = Insert, U = Update, D = Delete, S = Select. Видно, что процедур выборки минимум 2 на класс, одна из них предназначена для получения единственного элемента, а другая — для получения списка. Как они применяются будет показано в следующей статье.
И напоследок замечу, что после того, как манипуляции над элементом коллекции выполнены, необходимо сбросить все флажки его внутреннего состояния и в зависимости от количества выполненных операций удаления из БД удалить соответственное количество объектов из самой коллекции.