15. Шаблоны ассоциации
Ассоциации — распространенная конструкция в методах анализа и проектирования. Часто конкретная ситуация повторяется с ассоциацией. Может быть введена специальная нотация, но можно смоделировать ситуацию и без нее. Полезный способ рассуждать об этом — рассматривать ситуацию как паттерн. Этот ассоциативный шаблон можно представить в базовой форме или ввести новую нотацию в качестве сокращения. Оба варианта эквивалентны по смыслу.
Эта глава посвящена трем таким ситуациям. Ассоциативный тип (15.1) возникает, когда вы хотите рассматривать ассоциацию как тип, обычно наделяя ее некоторыми свойствами. Сопоставление по ключу (15.2) используется для того, чтобы придать сопоставлению поведение таблицы поиска или словаря. Каждый из этих паттернов использует множество методов с дополнительными обозначениями. Понимание паттернов, лежащих в основе обозначений, очень важно. Метод может не поддерживать дополнительные обозначения, поэтому важно знать, как работать без них. Это особенно актуально, если вы привыкли к методу, поддерживающему нотацию, и переходите к тому, который ее не поддерживает, или если вы переходите от одного метода к другому, а один из них не поддерживает нотацию.
Даже если в вашем методе используется нотация для ассоциативного паттерна, важно понимать, как нотация связана с более простыми идеями. Если ситуация встречается редко, часто лучше не вводить дополнительную часть обозначений для запоминания, а использовать базовую форму.
Третья ассоциативная модель — историческое сопоставление (15.3). Мы можем использовать исторические сопоставления для хранения истории изменений значений сопоставлений (например, истории зарплат сотрудника). Это не поддерживается специальной нотацией ни в одном из известных мне методов. Однако это жизненно важный паттерн для многих информационных систем. Когда требуется историческое сопоставление, может быть полезно ввести нотацию как сокращение для шаблона ассоциации. Особые сложности возникают, когда не только мир меняется, но и наши знания о нем меняются в разных местах; это приводит к двумерной истории (15.3.1).
На выбор между использованием нотации или базовой формы влияет несколько факторов. Концептуально основной компромисс заключается в краткости, предлагаемой нотацией, и дополнительными обозначениями, которые нам нужно помнить. В модели спецификации нотация подразумевает другой интерфейс в программном обеспечении. Этот интерфейс, вероятно, более удобен для использования, чем тот, который получается при преобразовании из базовой формы. Однако в спецификационную модель всегда можно добавить операции для обеспечения более удобного интерфейса. Это добавляет дополнительные явные операции в модель спецификации, но позволяет избежать лишней нотации.
Использовать нотацию или базовую форму — это вопрос выбора. В этой главе я излагаю свои предпочтения, которые, подчеркну, всегда стоят на втором месте по сравнению с желаниями клиента. Моя работа как консультанта заключается в том, чтобы облегчить жизнь клиента.
Ассоциативные паттерны работают на мета-уровне: это паттерны, которые используются в описании языков моделирования, а не самих моделей. Я использую термин «паттерны мета-модели» для описания этого общего класса паттернов. Другие шаблоны мета-моделей могут быть использованы для описания концепций мета-уровня в обобщении, моделях состояний или любой другой технике моделирования.
15.1 Ассоциативный тип
Распространенная ситуация при моделировании возникает, когда мы хотим добавить атрибут в отношение. Например, в ранней модели указано, что человек работает в компании, как показано на рис. 15.1. В ходе дальнейшей работы выясняется, что нам следует записывать день начала работы сотрудника, и он должен лежать в отношениях. Мы можем добавить атрибут даты начала в отношение, используя нотацию, подобную нотации Рамбо [2], как показано на рисунке 15.2.

Рисунок 15.1. Простые отношения между человеком и компанией.

На этой диаграмме используется нотация воротничка Рaмбо.
Рисунок 15.2 Добавление атрибута даты начала на рисунок 15.1
Если метод моделирования не поддерживает добавление атрибута к отношениям таким образом, существует ряд альтернатив. В нашем примере одной из альтернатив является добавление даты начала работы к персоне. Поскольку у человека, по определению, только одна компания, нет опасности возникновения двусмысленности. Мы могли бы подумать, что атрибут даты начала действительно является частью отношения, но это трудно обосновать кроме как семантической придирчивостью. Более разумное возражение заключается в том, что дата начала не должна иметь значения, если нет работодателя. Это можно решить с помощью правила, хотя такое решение всегда не слишком идеально, в частности потому, что большинство методов не очень хорошо поддерживают такого рода правила.
Этот подход нельзя использовать для отношений, в которых оба отображения являются многозначными, как на рис. 15.3. Поскольку у человека разные компетенции для каждого навыка, невозможно оценить номером человека.

Рисунок 15.3. Отношение, в котором оба отображения являются многозначными.
В методах, не поддерживающих ассоциативные типы, мы можем ввести дополнительный тип, как показано на рис. 15.4 (обратите внимание, как кардинальность перенесена с рис. 15.1). Решение в целом неплохое. Новый тип может быть несколько искусственным, но все модели содержат определенную долю искусственности, поскольку они представляют реальную ситуацию с большей степенью формальности, чем существует в естественном языке. Одно из самых значительных различий между двумя моделями заключается в интерфейсе. На рисунке 15.2 у person есть операция getEmployer, которая возвращает связанную с ним компанию. Модель на рис. 15.4 имеет другой интерфейс, который возвращает объект занятости. Объекту employment требуется дополнительное сообщение, чтобы получить компанию, поэтому нам нужно превратить исходную ассоциацию в производную, как показано на рис. 15.5.

Рисунок 15.4. Добавление типа занятости в качестве держателя даты начала.

Рисунок 15.5 Восстановление отображения работодателя с помощью производного отображения.
Мы можем рассмотреть более тонкий момент, рассмотрев ассоциацию «многие ко многим», показанную на рисунке 15.3. На рисунке 15.6 используется то же самое введение нового типа. Простое добавление типа компетенции хорошо работает на первый взгляд, потому что позволяет человеку иметь много компетенций, а значит, и много навыков, каждый из которых имеет значение компетенции. Проблема в том, что эта модель является более свободной, поскольку она также позволяет иметь несколько компетенций для одного и того же навыка. Чтобы устранить это, нам нужно дополнительное правило уникальности для компетенции, указывающее, что каждая компетенция должна иметь уникальную комбинацию человека и навыка.

Рисунок 15.6. Использование нового типа для работы с рис. 15.3
Эта проблема часто не замечается моделистами, использующими нотацию ассоциативного типа. Рисунок 15.7 представляет собой еще одно типичное использование этой нотации, в котором отношения поддерживают понимание того, что человек может быть работником многих компаний, и некоторые из этих работ могут быть завершены, так что у нас есть история занятости. Вполне возможно, что человек может иметь два периода работы в одной компании. Поэтому мы не будем добавлять ограничение в стиле, показанном на рис. 15.6. Проблема в том, что в общем случае мы не знаем, интерпретировать ассоциативный тип как имеющий ограничение или нет.

Рисунок 15.7. Трудоустройство ассоциативного типа.
На практике моделисты используют нотацию ассоциативного типа с обеими интерпретациями. Это само по себе не является недостатком, но они должны четко указывать, что именно они имеют в виду. Разумно использовать рисунок 15.7, но в этом случае для случая рисунка 15.3 должно быть использовано правило, аналогичное правилу на рисунке 15.6. Если разработчики моделей хотят использовать случай рис. 15.3 в качестве обычной интерпретации, то они не могут использовать модель в форме рис. 15.7; вместо этого они должны использовать новый тип.
В целом я не склонен использовать обозначения ассоциативных типов. Если они не включают определенное правило, такое как правило уникальности, то я не думаю, что они добавляют много пользы от дополнительных обозначений. Уникальность может быть полезна, но она так редко используется должным образом, что я бы предпочел использовать дополнительный тип и добавить правило уникальности, чтобы сделать его явным.
15.2 Сопоставление по ключу
Сопоставления с ключами представляют собой технику, которая в анализе повторяет технику использования словарей (индексированных таблиц поиска, также называемых словарями [1] или ассоциативными массивами) для реализации отношений. Примеры ее использования показаны на рис. 15.8 и 15.9. Наша основная задача — записать, сколько определенного товара находится в конкретном заказе. Классическая модель данных для этого показана на рис. 15.8. Модель, показанная на рис. 15.9, использует нотацию отображения с ключом, которая концентрируется на запросе заказа о количестве товара и его изменении. На рисунке 15.8 это уравновешивается тем, что продукт может ответить, в каких заказах он заказывается и сколько в каждом заказе.

Рисунок 15.8. Классическая модель заказа, линейный элемент.

Рисунок 15.9 Использование словаря для моделирования рисунка 15.8.
Важной частью интерпретации этих моделей является то, как они влияют на интерфейс типов. Модель на рис. 15.8 подразумевает интерфейс getLineItems
для Заказа и Продукта. Модель на рис. 15.9 подразумевает интерфейс getAmount-(product)
для Заказа. Для товара интерфейс не подразумевается. Чтобы найти использование продукта в разных заказах, нужно спросить у всех экземпляров заказа, есть ли у них сумма для продукта, что несколько сложнее. Еще одно отличие заключается в том, чтобы спросить у заказа, какие продукты в нем присутствуют. На рис. 15.8 для этого достаточно запросить у заказа его строковые позиции, а затем у каждой строковой позиции — ее продукт. Для рисунка 15.9 это потребует запросить у заказа словарь сумм, а затем запросить его ключи; заказ должен будет предоставить операцию getAmounts
, чтобы обеспечить доступ к своему словарю (или, более строго, к его копии). В противном случае нам придется проверять каждый экземпляр продукта на соответствие заказу.
Нотация отображения с ключом может использоваться для обработки ограничений уникальности. Модель на рис. 15.8 обычно включает правило, гласящее, что для товара в заказе может существовать только одна позиция. Мы не хотим, чтобы в одном и том же заказе была линейная позиция для 30 виджетов и отдельная линейная позиция для 20 виджетов. Лучшее предложение — иметь одну позицию для 50 виджетов. Это требует правила для рисунка 15.8, но вполне очевидно на рисунке 15.9, поскольку в заказе может быть только один тип «количество» товара».
Нам нужно подумать, какой ответ должен дать заказ, если его спросят о количестве товара, которого нет в заказе. В данном примере разумно вернуть 0
, сделав ключевое сопоставление обязательным. В других случаях мы можем захотеть вернуть null
, что сделает сопоставление необязательным.
Если оба представления ценны, то нет причин, по которым мы не можем использовать их вместе. Мы можем отметить избыточность с помощью правила или маркера деривации, как показано на рис. 15.10. Использование обоих представлений подтверждает тот факт, что подход на рис. 15.8 является более гибким в общих случаях, а подход на рис. 15.9 добавляет очень полезное сокращенное поведение, а также делает уникальность явной.

Рисунок 15.10. Используя оба представления, обозначьте одно из них как производное.
Я считаю нотацию отображения с ключом очень полезной конструкцией. Использую ли я ее или дополнительный тип, зависит от ситуации и от того, что я хочу подчеркнуть. Хотя я, конечно, могу жить и без нее, я часто нахожу ее удобной конструкцией. Остерегайтесь, однако, не злоупотреблять ею. Часто дополнительный тип важен для получения дополнительной информации и поведения. На рисунке 15.8 мы можем легко добавить стоимость для статьи, что было бы неудобно при использовании рисунка 15.9. Естественно, ответ «усидеть на двух стульях сразу», представленный на рисунке 15.10, является частым выбором.
15.3 Историческое сопоставление
Объекты не просто представляют предметы, существующие в реальном мире; они часто представляют воспоминания о предметах, которые когда-то существовали, но потом исчезли. Использование объектов для представления воспоминаний вполне допустимо — воспоминания о существовании часто так же реальны для людей, как и само существование — но важно уметь различать их. Рассмотрим вопрос о записи заработной платы человека. В любой момент времени у человека есть одна зарплата, как показано на рис. 15.11. Однако с течением времени эта зарплата может измениться. Само по себе это не делает рисунок 15.11 недействительным в качестве модели, если только нам не нужно помнить историю зарплаты. Если все, что нам нужно, — это помнить прошлые зарплаты, то рисунок 15.12 подойдет для этого, если мы добавим к модификатору зарплаты возможность добавлять старую зарплату в список старых зарплат. Используя список, мы можем не только записать предыдущие зарплаты, но и сохранить порядок, в котором они были начислены.

Рисунок 15.11. В любой момент времени у человека есть одна зарплата.

Рисунок 15.12. Модель, запоминающая прошлые зарплаты.
Рисунок 15.12 может быть полезным во многих ситуациях, но он не поможет нам ответить на вопрос «Какова была зарплата Джона Смита 2 января 1997 года?». Чтобы ответить на этот вопрос, нам нужен более сложный подход, предложенный на рис. 15.13. Эта модель дает нам возможность записывать как зарплаты, так и их полную историю. Однако нам необходимо дополнительное правило: зарплата одного человека не должна иметь перекрывающиеся временные периоды. Это правило часто неявно предполагается, но обычно не показывается в явном виде — и поэтому о нем забывают.
Модель, показанная на рис. 15.13, обеспечивает необходимую нам мощность, но она довольно неуклюжа. Важный момент, что у сотрудника может быть только одна зарплата одновременно, теряется без рассмотрения основных правил. Одна ассоциация между двумя типами теперь представляет собой четыре типа и три ассоциации. Это может значительно усложнить диаграмму, особенно если таких исторических связей много. Интерфейс, предложенный для этого, также довольно неуклюж. Ответ на вопрос в предыдущем абзаце предполагает запрос у Джона Смита всех его зарплат, а затем выбор той, временной период которой включает 2 января 1997 года.

Рисунок 15.13. Полная запись истории заработной платы.
Я часто использую модель, показанную на рис. 15.14, которая сочетает в себе гибкость подхода, описанного на рис. 15.13, и диаграммную экономию снимка на рис. 15.11. Все детали скрыты за небольшим, но значимым ключевым словом [history]. Я ввел новую нотацию, которая вполне допустима при условии, что я правильно ее определяю. Я откажусь от математического определения и вместо этого укажу интерфейс, определяемый ключевым словом. Рисунок 15.11 предполагает наличие аксессора getSalary()
для возврата значения зарплаты и модификатора setSalary(Money)
для его изменения. На рисунке 15.14 представлен другой интерфейс: Аксессор getSalary()
по-прежнему существует, но на этот раз возвращает текущее значение отображения зарплаты. Это поддерживается функцией getSalary(Date)
, которая возвращает значение отображения на указанную дату. getSalary()
эквивалентна getSalary(Date::now)
.
Обновление немного сложнее. Мы можем использовать операцию setSalary(Money, Date)
, чтобы добавить в историю новую зарплату, начиная с определенной даты. Это хороший интерфейс для аддитивных изменений, но его недостаточно, если нужно изменить старую запись. Лучше всего использовать операцию setSalaryHistory(Dictionary (key: TimePeriod, value:Money))
вместе с операцией getSalary-History()
. Тогда клиент сможет получить текущую историю зарплат в виде словаря, использовать стандартные операции со словарем, а затем изменить всю запись за один раз. Это лучше, чем обновлять по одной записи за раз, поскольку существует правило, согласно которому сотрудник должен иметь одну зарплату на одну дату. Если изменения вносятся по одной записи за раз, неудобно сохранять правило верным после каждого изменения. Вытащить всю запись, изменить ее (без проверки правила) и заменить за один раз гораздо проще.

Рис. 15.14 Представление мощности на рис. 15.13 с помощью более простой нотации.
Очевидно, что здесь предлагается реализация словаря с ключами временных периодов. Такая реализация легко поддерживает все поведение, требуемое интерфейсом, и является простым применением подхода. Мы можем даже пойти дальше и ввести специальный класс для работы с историческими коллекциями.
Насколько мне известно, нотация истории в настоящее время не предлагается ни одним методистом. Она очень ценна, потому что упрощает ситуацию, которая является одновременно и распространенной, и сложной. Идеальным решением было бы создание объектной системы с полной возможностью «путешествия во времени». Такая система не является совсем уж надуманной, и ее появление устранит необходимость в какой-либо специальной обработке истории.
Этот раздел также является частным случаем общего положения. При моделировании вы можете столкнуться с повторяющейся ситуацией, которая одновременно и распространена, и неудобна для моделирования. Не бойтесь ввести новую нотацию, чтобы упростить ее, но вы должны правильно ее определить. Ключевой компромисс, который следует учитывать, — это упрощение новой конструкции по сравнению с необходимостью запоминать дополнительные обозначения. Хорошая нотация — это компромисс, позволяющий добиться элегантности, но без обширной нотации. Компромисс не одинаков для всех проектов, поэтому не бойтесь принимать собственные решения в этих вопросах.
15.3.1 Двухмерная история
Выше обсуждалась проблема получения значений некоторых атрибутов объекта в определенный момент в прошлом. Многие системы имеют дополнительные сложности, связанные с тем, что они не получают информацию об изменениях своевременно.
Представьте, что у нас есть система расчета заработной платы, которая знает, что с 1 января ставка сотрудника составляет 100 долларов в день. 25 февраля мы проводим платежную ведомость с этой ставкой. 15 марта мы узнаем, что с 15 февраля ставка сотрудника изменилась на $110 в день. Что должен ответить объект работника на вопрос, какова была его ставка на 25 февраля? На этот вопрос есть два ответа: какая ставка, по мнению сотрудника, была в то время и какая, по мнению сотрудника, ставка сейчас. Обе эти ставки важны. Если нам нужно просмотреть платежную ведомость за 25 февраля, чтобы понять, как рассчитываются цифры, нам нужно увидеть старую цифру. Если нам нужно обработать новое право, возможно, на пару часов сверхурочной работы, о которых не сообщалось ранее, нам нужна ставка, как мы ее понимаем сейчас.
Жизнь такова, какой она есть, и все может стать еще хуже. Предположим, что мы внесли соответствующие корректировки и произвели выплату за просроченные сверхурочные, и все это было обработано в платежной ведомости 26 марта. 4 апреля нам сообщают, что ставка сотрудника была снова изменена на 112 долларов с 21 февраля. Теперь объект «Сотрудник» может дать три ответа на вопрос о том, какой была его ставка 25 февраля!
Чтобы справиться с подобной проблемой в общем случае, нам нужна двумерная история. Мы спрашиваем работника, какой была его ставка в какой-то момент в прошлом, согласно нашим знаниям в какой-то другой момент в прошлом. Таким образом, необходимы две даты: дата, на которую действует ставка, и дата, на которую мы опираемся в своих знаниях, как показано в табл. 15.1.
Дата применения | Дата знания | Результат |
---|---|---|
25 февраля | 25 февраля | $100/день |
25 февраля | 26 марта | $110/день |
25 февраля | 26 апреля | $120/день |
Таблица 15.1 Двумерные ставки для примера.
В одномерном примере фактически приходится выбирать между тем, чтобы считать даты применимости и знания одинаковыми, или всегда считать дату знания «сейчас».
Добавление полных двумерных возможностей в историю, конечно, значительно усложняет ее, и не всегда это оправдано. Важно рассмотреть, зачем могут понадобиться эти разные показатели. В данном примере единственной причиной, по которой нам нужно знать что-то, кроме наших текущих знаний о прошлом, может быть объяснение и проводка корректировок предыдущих зарплатных ведомостей. Другим способом решения этой проблемы может быть встраивание всей информации о том, как производится расчет заработной платы, в результат расчета заработной платы. Если эта информация будет проверяться только человеком и не будет обрабатываться, ее можно оформить в виде текстового атрибута. Вычисление корректировок может быть выполнено путем ссылки на результаты расчета — ставка, которая была использована, не обязательно нужна. Даже если ставка необходима, создание копии может считаться более безопасным. При наличии всего этого требуется только одномерная история, чтобы можно было обрабатывать ретроактивные выплаты (например, за два часа сверхурочной работы, о которых было сообщено с опозданием).
Двухмерная история также влияет на временные точки, которые ставятся на события. Если мы не уверены, что всегда знаем, как только произойдет событие, нам нужны две временные точки для любого события: точка, когда событие произошло, и точка, когда наша система узнала о событии. (В качестве примера можно привести две временные точки при входе в систему, рассмотренные в разделе 6.1, и двойные временные записи, рассмотренные в разделе 3.8).
Ссылки
Musser, D.R., and A. Saini. STL Tutorial and Reference Guide. Reading, MA: Addison-Wesley, 1996.
Rumbaugh, J. “OMT: The object model.” Journal of Object-Oriented Programming, 7, 8 (1995), pp. 21–27.