Страницы

Поиск по вопросам

четверг, 4 октября 2018 г.

Domain Model + Data Mapper: сохранение коллекции связанных объектов

В некотором текстовом редакторе для представления документа и его правок используется реализация шаблона проектирования Модель Предметной Области (Domain Model).
Иерархия моделей предметной области имеет следующий вид (лишние детали опущены):

Здесь, экземпляр документа (Document) управляет своими изменениями (Change), связанными в коллекцию изменений (ChangeCollection). После внесения изменений в документ, вызывается метод Document::applyChanges при этом не важно кто именно делает этот вызов (сам объект Document или клиентский код). В случае, если предыдущее изменение было сделано тем же пользователем, что и текущее, последнее изменение в истории правок обновляется. Иначе создается новое изменение.
Работа с правками проводится преимущественно в экземпляре Document, но сами правки могут быть переданы клиентскому коду. Примером такой передачи может служить метод Document::getHistory, возвращающий всю историю правок документа (например, для построения журнала изменений). Это означает, что любая правка может быть изменена (например, через Change::setDescription) без явного уведомления об этом факте коллекции правок (ChangeCollection).
Для сохранения этой иерархии классов в БД используется реализация шаблона Преобразователь данных (DataMapper):

В этой схеме, DocumentMapper имеет ссылку на ChangeMapper чтобы иметь возможность загружать/сохранять связанные изменения. При вызове DocumentMapper::save автоматически вызывается ChangeMapper::save для каждого элемента коллекции Document::history

Проблемы начинаются, когда у каждого документа может быть не одна правка, а несколько сотен. Принудительное сохранение каждой правки приводит к существенным расходам машинного времени.
Собственно вопрос: каким образом правильнее всего учитывать изменения правок и сохранять только новые/удаленные/измененные?
Сразу оговорюсь, интересуют именно проектные решения, не основанные на специфических приемах того или иного языка программирования.


Ответ

TL;DR
Самое простое решение — отслеживать изменение экземпляров Change собственными средствами этого класса. Для этого можно создать метод Change::isModified. А изменение состава коллекции проще всего отслеживать на уровне самой коллекции.
Исследование по теме вопроса
После проведения небольшого исследования по теме вопроса оказалось, что есть сразу несколько способов решения проблемы. Они отличаются как по степени гибкости, так и по педантичности разделения функционала системы на отдельные "слои". Данное исследование не претендует на полноту, поэтому я мог что-то упустить.
К сожалению, ни одно из этих решений не прозвучало в ответах на вопрос, поэтому приведу их все.
Реализовать контроль добавления/удаления объектов в коллекции (ChangeColletion), а контроль изменений в самих объектах Change
Этот путь невероятно прост, особенно если для доступа к свойствам модели Change используются только специальные геттеры/сеттеры. Из минусов этого решения можно выделить необходимость внесения в интерфейс класса Change метода isModified, который не имеет ценности в рамках бизнес-логики приложения.
UML диаграмма* для этого случая может иметь вид:
Реализовать контроль добавления/удаления объектов в коллекции (ChangeColletion) (аналогично п. 1). При этом, контроль изменения объектов Change проводить в коллекции ChangeCollection на основе заранее сохраненных "эталонных копий" элементов коллекции.
Из минусов можно выделить необходимость хранения сразу двух объектов Change для каждого из элементов коллекции: эталонного и изменяемого. Кроме того, необходимо реализовать механизм сравнения объектов Change, что в ряде случаев может быть проблематично.
UML диаграмма классов* для этого случая может иметь вид:
Использовать классическую реализацию шаблона Единица Работы (Unit of Work).
Экземпляр единицы работы создается в ChangeMapper при загрузке ChangeCollection и регистрируется в экземплярах ChangeCollection и Change. При каждом изменении состояния коллекции, она уведомляет об этом экземпляр UnitOfWork. В свою очередь, при изменении экземпляров Change они так же сообщают об этом факте. При сохранении коллекции элементов в БД, экземпляр UnitOfWork извлекается из коллекции элементов и на его основе генерируются традиционные Create/Update/Delete запросы к БД.
Из минусов стоит отметить некоторую громоздкость реализации, и необходимость в явном виде передавать экземпляр UnitOfWork всем заинтересованным сторонам. API классов Change и ChangeCollection (как, впрочем, и логика их работы) так же пополнятся дополнительными методами имеющими смысл только для правильного сохранения в БД.
UML диаграмма классов* для этого случая может иметь вид:
Реализовать шаблон проектирования Наблюдатель (Observer) в классах Change и ChangeCollection
При каждом изменении состава коллекции элементов или состояния самих элементов создается событие, которое может быть обработано любым участником системы. При загрузке объектов в экземпляре ChangeMapper этот экземпляр регистрирует себя как наблюдатель, и ведет "журнал изменений" состояния ChangeCollection и вложенных Change. При сохранении именно этот журнал и используется для определения новых/удаленны/измененных объектов Change. При желании, функционал "журнала изменений" можно вынести в отдельный класс и использовать композицию для его связи с ChangeMapper
Этот подход является наиболее общим. Новый API, связанный с реализацией шаблона Наблюдатель может быть легко вовлечен в бизнес-логику приложения. Однако нужно понимать, что если слой бизнес-логики не проектировался для работы с событиями с самого начала, то API наблюдения все так же будет использоваться только для сохранения объектов в БД. Другой недостаток подобного решения заключается в увеличении общей сложности системы
UML диаграмма классов* для этого подхода может иметь вид:
Можно использовать не изменяемые объекты Change
При необходимости внесения изменений создается новый экземпляр Change, который регистрируется в коллекции ChangeCollection с помощью ее же методов. Тогда, вся информация о изменениях доступна в экземпляре коллекции и может быть легко получена при сохранении. Например, можно хранить ссылки на оригинальный состав коллекции и при необходимости сравнивать их со ссылками на объекты из текущего состава. (Сравнение ссылок на объекты существенно проще, чем сравнение самих объектов, поэтому нет нужны в лишней памяти для хранения дубликатов объектов как в п. 2.)
При желании, можно реализовать шаблон Декоратор (Decorator) для изоляции собственно коллекции, и логики отслеживания ее изменений.
Из сложностей, стоит отметить неестественность такого подхода в традиционных ОО-системах. Это скорее путь радикального функционального программирования.
UML диаграмма* такого решения может иметь вид:

* Все приведенные UML диаграммы существенно упрощены. Из них выброшено все, что не относится на прямую к делу.

Комментариев нет:

Отправить комментарий