Проблемы моделирования предметной области на основе ООП

Демид Тузенко, март 2002
с редакторскими правками, февраль 2005

Аннотация

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

Прим. редактора: в данной работе освещаются вопросы, которые пытаются решить в явном виде современные реализации средств объектно-реляционного отображения Object-Relational Mapping (ORM) и Object Persistence Frameworks (OPF).

Вступление

«… За последние несколько лет объектно-ориентированная технология проникла в различные разделы компьютерных наук. К ней относятся как к средству преодоления сложности, присущей многим реальным системам. Объектная модель показала себя мощной объединяющей концепцией.»
Гради Буч. Из предисловия ко второму изданию книги «Объектно-ориентированный анализ и проектирование с примерами приложений на C++»

Со времени разработки концепции объектно-ориентированного программирования прошло много лет. Действительно, ООП стало необходимым элементом почти любой компьютерной технологии. Однако, в последнее время становится все более наглядным один факт: полностью на основе объектных технологий строятся только различные инструментальные средства, и практически никогда – конечные коммерческие решения, связанные с обработкой данных. Причина такого безрадостного положения вещей в неоправданной сложности систем, построенных на базе ООП. Именно в той самой сложности, во имя борьбы с которой и была создана концепция ООП. Положение усугубляется еще и тем, что при всей сложности будущего решения ООП создает иллюзию простоты, и не последнюю роль здесь сыграли «классические» трубы вроде того, цитата из которого приведена в начале абзаца. Разработчик, решивший строить очередной проект на основе средств ООП впоследствии сталкивается с таким ворохом проблем (зачастую даже четко не осознаваемых), что это навсегда отбивает охоту к подобным экспериментам.

Мы попытаемся развенчать миф о простоте объектно-ориентированного подхода к проектированию информационных систем. Изложение материала начнем с рассмотрения модель простейшей адресной книги, и покажем, что даже для такой простой задачи решение в духе ООП выглядит мягко говоря нетривиально, а его сложность превосходит все разумные границы и не выдерживает сравнения с другими методами программирования. Также мы попытаемся проанализировать причины такой сложности и наметить пути, которые бы позволили совместить простоту подхода, свойственную ООП, с получением работоспособного результата.

Моделирование

Пусть в адресной книге для простоты будут находиться только записи о контактах и их номерах телефонов. Спроектировать объектно-ориентированную модель такой адресной книги несложно с использованием любой известной методики. Опишем модель в виде статической диаграммы UML (Рисунок 1).

Рисунок 1 Предметная область адресной книги

Теперь можно взять любую объектно-ориентированную среду программирования (к примеру, Object Pascal – Delphi) и перевести эту модель на язык программирования. Итак, запускаем Delphi и описываем согласно модели:

type
  TPhoneNumber = string; { для простоты пусть будет строка }
  TPhone = class;
  TContact = class;
 
  TAddressBook = class
  private
    function GetContact(Index: integer): TContact;
    procedure SetContact(Index: integer; Value: TContact);
  public
    property Contacts[Index: integer]: TContact
      read GetContact write SetContact;
  end;
 
  TContact = class
  private
    function GetPhone(Index: integer): TPhone;
    procedure SetPhone(Index: integer; Value: TPhone);
  public
    FirstName: string;
    LastName: string;
    MiddleName: string;
    property Phones[Index: integer]: TPhone
      read GetPhone write SetPhone;
  end;
 
  TPhone = class
  public
    Number: TPhoneNumber;
  end;

(чтобы не загромождать текст, реализацию приводить не будем, тем более что пример гипотетический и очень простой)

Сделали. Нарисовали пользовательский интерфейс, отладили, работает.

Теперь проведем эксперимент. Что произойдет, если мы запустим вторую копию созданного приложения? Легко предугадать, что вторая копия приложения будет точно так же правильно функционировать, но только в рамках своих собственных данных, никак не связанных с первой копией. Но предметная область ведь одна! Как же так? И проектировали правильно, и модель верна, и реализовали без ошибок, а получилось не то, чего ожидали. Эту загадку в два счета раскроет любой программист. Достаточно выполнить так называемый reverse engineering созданного приложения, чтобы увидеть суть проблемы. Впрочем программист, даже с минимальным опытом работы, не будет вытаскивать на свет такие страшные термины. Он просто скажем вам, что каждое запущенное приложение работает в своем пространстве, и TAddressBook первого запущенного приложения будет совсем не тот же самый, что и TAddressBook второй копии приложения, равно как и экземпляры этих классов. Таким образом мы получили следующую схему (Рисунок 2). Здесь мы ввели еще и интерфейс пользователя, потому что договорились, что приложение его содержит. Впрочем, никаких утверждений по поводу его поведения и реализации делать не будем.

Рисунок 2 Адресная книга с использующим ее приложением

Теперь ясно, почему в нескольких выполняющихся копиях приложения могут быть определены разные данные о предметной области. Но ведь в первоначальной модели ничего не говорилось о возможности оперировать несколькими наборами данных! Эта неувязка имеет несколько способов решения. Во-первых, можно запретить запуск более одной копии приложения. Некрасиво и неправильно со всех точек зрения, тем более что обычно адресная книга выполняется не сама по себе, а в составе других приложений, которым нужен доступ к репозиторию адресов, и это не единственный пример. Во-вторых, можно вынести объекты за пределы приложения, в единое хранилище, и обращаться к одному объекту из нескольких приложений. Такие средства есть (наиболее известные – DCOM, CORBA), но они обладают существенными недостатками:

  1. Производительность. Обращение к данным, находящимся вне выполняемого процесса, происходит через обращение к ядру операционной системы. Это не является серьезным препятствием для приложений, оперирующих небольшим количеством объектов, но совершенно неприемлемо для сложных приложений, обрабатывающих большие массивы данных.
  2. Поведение объектов также переносится за пределы приложения (хоть мы и рассматриваем в качестве примера объекты, не имеющие собственного поведения, это не типичная ситуация). В некоторых случаях это также не допустимо.

И, наконец, в третьих, самый распространенный и самый эффективный метод, использующийся не только в ООП, но и при других подходах к программированию —создание рабочих копий объектов в среде приложения. Рабочие копии объектов – это экземпляры программных классов, выполняющихся в среде приложения и имеющих такую структуру, которая позволяет отображать в них состояние и реализовывать поведение объектов предметной области. Таким образом мы решили сразу две задачи – эффективный доступ к данным и перенос поведения объектов в пределы приложения. Отобразим необходимость использования рабочих копий объектов на нашей диаграмме. Очевидно, рабочие объекты должны быть определены для каждого объекта предметной области, и находиться в тех же отношениях друг с другом, что и объекты предметной области (Рисунок 3).

Рисунок 3 Адресная книга с учетом рабочих объектов

Теперь сравним эту диаграмму с предыдущей. Очевидно, что то место, которое на предыдущей диаграмме занимали объекты предметной области, теперь заняли рабочие объекты. То есть с помощью программного кода, описанного выше, мы описали не столько объекты ОО-модели, сколько рабочие объекты приложения. Зато теперь все выглядит правильно – приложение работает с рабочими объектами, которые описываются и программируются с помощью имеющихся средств разработки. А где же теперь объекты предметной области? Собственно, там же, где и были — в предметной области. Данный пример наглядно показывает, что средства ООП, введенные в языки программирования, не могут служить средством описания объектов предметной области для использования их в разрабатываемых программах. Они (средства ООП) служат лишь для описания программных классов и объектов, которые могут с разной степенью адекватности представлять объекты предметной области, а также могут обладать своим собственным состоянием и поведением, не свойственным представляемым ими объектам предметной области. Например, естественное свойство программного объекта «Адресная книга», или, как мы его называли ранее, «Рабочий объект «Адресная книга»» — существовать в нескольких экземплярах (по одному на каждую копию запущенного приложения) — несвойственно объекту предметной области «Адресная книга». Исключения из этого утверждения все же есть, но сейчас мы на этом останавливаться не будем.

Раз уж зашла речь о свойствах программных объектов, не свойственных объектам предметной области, стоит упомянуть еще одно, крайне неприятное. Программные объекты существуют только в адресном пространстве приложения, и только во время работы приложения. При его закрытии операционная система обычно освобождает все ресурсы, выделенные приложению, даже если программист не позаботился об этом явно. Это означает, что все данные, которые мы вводим, модифицируем, просматриваем, актуальны только тогда, когда мы с ними работаем. Разумеется, для абсолютного большинства задач такое положение совершенно неприемлемо. Выход из создавшейся ситуации тоже очевиден на первый взгляд. Нужно сохранять данные на каком-либо внешнем, долговременном носителе информации. Что выбрать в качестве такого носителя? Этот вопрос не слишком важен принципиально, но мы все же рассмотрим некоторые варианты. Во-первых, можно использовать обычные файлы. Недостатки такого подхода очевидны практически сразу после того, как мы приступим к его реализации. А именно – использование неструктурированных файлов отбрасывает нас на шаг назад, поскольку мы вынуждены работать не с логическими единицами данных (числа, строки и проч,), а с физическими – байтами, блоками. К тому же на нас разом перекладываются все вопросы обеспечения целостности, поиска и других необходимых функций. Во-вторых, существуют пока еще немногочисленные объектно-ориентированные СУБД. Как правило, они имеют свой собственный метод описания объектов, не совместимый с принятыми в языках программирования, собственный язык запросов и прочие особенности. Но главное – нет единого стандарта на такие средства хранения информации, и уровень их реализации оставляет желать лучшего. И третий способ – использовать реляционную СУБД, в частности SQL-сервер. Преимущества очевидны – рынок РСУБД развивался не одно десятилетие, существуют множество готовых решений (как коммерческих, так и бесплатных), и почти каждый разработчик ПО в той или иной степени знаком с принципами и имеет опыт работы с такими СУБД. Итак — РСУБД, хотя, подчеркнем еще раз: выбор средства хранения данных не имеет принципиального значения в обсуждаемом контексте, просто дальнейшие рассуждения будет удобнее базировать на чем-то конкретном.

Проектирование структуры реляционной базы данных – давно и хорошо изученная область. Мы не будем на этом останавливаться, отметим только, что для организации хранилища данных для нашей задачи нам потребуется две таблицы: «Контакты» и «Телефоны». Записи этих таблиц обладают всеми свойствами, которых недостает описанным выше рабочим объектам: они независимы от приложения и не уничтожаются при закрытии приложения или выключении компьютера. Следовательно, эти записи имеют все необходимое, чтобы отображать состояние не только рабочих объектов, но и объектов предметной области. Отобразим приведенные рассуждения на диаграмме (Рисунок 4). Отметим, что введенные новые элементы, так же как и введенный ранее элемент «интерфейс пользователя», классами в привычном смысле этого слова не являются. Однако, они отображены как классы, поскольку, на концептуальном уровне, обладают состоянием, поведением, и в достаточной степени инкапсулируют свои свойства.

Рисунок 4 Адресная книга с базой данных

Теперь мы получили нечто материальное для отражения объектов предметной области. Но рабочие объекты и записи таблиц базы данных связаны между собой только через предметную область, которая, как уже было выяснено, для приложения недоступна и выполнять полезной работы по передаче информации не может. Следовательно, должен существовать какой-то механизм передачи данных между записями БД и рабочими объектами. Эти функции обычно возлагаются на приложение, а та часть его кода, которая отвечает за эти вопросы, имеет уже устоявшееся название «Контур хранимых объектов» (“Object Persistence Framework”, OPF). Отобразив этот компонент на нашу диаграмму, мы наконец-то получим первую работоспособную модель адресной книги, учитывающую реалии имеющихся средств программирования и хранения информации (Рисунок 5). Назовем этот тип модели рабочей моделью. Мы намеренно не указали мощность отношений между OPF и приложением, OPF и базой данных, поскольку не делали никаких допущений об устройстве и принципе работы OPF.

Рисунок 5 Рабочая модель адресной книги

Такая модель уже в принципе пригодна к реализации с помощью какой-либо системы программирования (разумеется, при достаточно полном и четком определении понятий «Приложение», «Интерфейс пользователя», «OPF» и других, обозначенных на диаграмме лишь концептуально). Однако, давайте сравним нашу первоначальную модель (Рисунок 1) и последнюю, работоспособную (Рисунок 5). Можно даже привести их вместе для большей наглядности (Рисунок 6).

Рисунок 6 Сравнение моделей

Как видно, сравнение сложности и громоздкости явно не в пользу рабочей модели. Вместе с тем, давайте внимательнее посмотрим на концептуальную модель (Рисунок 1). Есть ли в ней что-то неправильное, неполное или неточное? Нет! Модель адекватно отображает предметную область в той мере, в которой мы ее обозначили. Эта модель не учитывает только одного: у нас нет средств, позволяющих ее реализовать. Разработка таких средств либо находится в зачаточном состоянии, либо вообще не ведется, но современные средства программирования работать с такими моделями не могут. Назовем такую модель канонической и дадим определение этому термину:

Канонической моделью называется объектно-ориентированная модель предметной области, которая рассматривает только объекты предметной области и не учитывает аспекты и возможные трудности ее реализации в объектной среде.

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

Итак, мы составили каноническую и рабочую ОО-модели предметной области адресной книги. Легко подсчитать, что рабочая модель оперирует четырнадцатью объектами против трех в канонической модели, и семнадцатью связями против двух. Наглядно видно увеличение сложности почти на порядок. И это только простейшая модель! Легко предвидеть, что каноническая модель даже средней сложности, включающая 100-200 объектов, при аналогичном преобразовании в рабочую модель перерастет все разумные границы сложности и станет практически нереализуемой. Выход в этой ситуации мы видим в создании таких средств (шаблонов, стереотипов, методик, библиотек), которые позволяли бы реализовывать каноническую модель непосредственно (в идеале), либо выполнять преобразование канонической модели в рабочую таким образом, чтобы уровень сложности при этом существенно не возрастал.

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

Прежде всего следует отметить некоторую неопределенность в описании связей между элементами рассмотренной канонической модели. А именно: композиция между адресной книгой и контактом обозначает не включение класса «Контакт» в класс «Адресная книга», а указание того факта, что объект класса «Адресная книга» может включать в себя ноль и более объектов «Контакт». Такое же отношение определено и для композиции «Контакт–Номер». Теперь, добавив сюда известное отношение между классом и объектами этого класса как 1:n (Рисунок 7) можем составить «модель модели» адресной книги (Рисунок 8). Замечание по нотации: согласно описанию UML обозначение экземпляра класса отличается от обозначения класса тем, что название экземпляра подчеркнуто непрерывной линией и через двоеточие может указываться название класса, экземпляром которого является этот объект.

Рисунок 7 Отношение класса и объекта как экземпляра этого класса

Рисунок 8 Модель классов адресной книги

Аналогичные действия нужно провести и для рабочих объектов. Классы рабочих объектов назовем «Программными классами» (потому что они и на самом деле являются программными классами, такими как определенные в начале TAddressBook, TContact и TPhone), а экземпляры программных классов назовем для краткости просто «Экземплярами». Получившаяся диаграмма (Рисунок 9) почти в точности повторяет модель классов адресной книги (Рисунок 8), что совершенно естественно — программные классы, по уже приведенному определению, имеют такую структуру, которая позволяет отображать в них состояние и реализовывать поведение объектов предметной области.

Рисунок 9 Модель программных классов адресной книги

Остальные элементы рабочей модели преобразованию по подобной логике не подлежат, поскольку не являются обычными классами. Тем не менее это не помешает нам провести дальнейшую декомпозицию и поиск абстракций над этими элементами. Совершенно очевидно, что «Запись телефона» и «Запись контакта» являются частными случаями более общего понятия. Назовем его «Запись» и дадим ему следующее определение:

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

Здесь мы уже смогли отойти от терминологии РСУБД, как было отмечено, тип хранилища информации в рассматриваемых моделях существенной роли не играет. Еще один существенный момент: запись может выполнять функции хранилища данных объекта только в том случае, если в информационной системе есть средства, позволяющие эти данные интерпретировать. По аналогии с файлом операционной системе: файл содержит полезную информацию только тогда, когда в системе установлено приложение, способное прочитать и интерпретировать содержимое этого файла.

Продемонстрируем понятие записи при помощи диаграммы UML (Рисунок 10). Здесь не показаны отношения между классами и экземплярами классов, они были описаны ранее (Рисунок 8). Если исключить экземпляры конкретных классов, а также метаклассы «Запись» и «Класс» (в UML отношения между экземплярами классов отображаются как отношения между классами), получим гораздо более простое описание понятия записи (Рисунок 11). Здесь также принято во внимание, что отношение 0..1:1 было принято нами искусственно, для упрощения описания рабочей модели адресной книги. В действительности же нередки ситуации, когда информация об одном объекте дублируется в нескольких местах, например, чтобы избежать лишней перекачки данных через глобальную сеть, или для обеспечения сохранности данных. Поэтому в последнем определении одному объекту может соответствовать несколько записей.

Рисунок 10 Подробная диаграмма, иллюстрирующая понятие "Запись"

Рисунок 11 Упрощенная (рабочая) диаграмма отношения записи и объекта предметной области

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

Исполнением называется экземпляр программного класса, состояние которого соответствует состоянию какого-либо объекта предметной области, и существуют средства идентификации (сопоставления) этого исполнения с объектом предметной области, который оно представляет.

Проиллюстрируем это определение диаграммой (Рисунок 12). Отдельного пояснения заслуживает мощность отношения исполнения и объекта (0..1:0..n). В общем случае объект предметной области может не существовать в то время, когда существует исполнение (например, он существовал в прошлом, когда исполнение было создано, или будет существовать в будущем, когда завершиться обработка транзакции). Диаграмма не позволяет отобразить такие тонкости, поэтому со стороны объекта указано 0..1. Исполнения же принадлежат приложениям, которых может быть запущено несколько, даже на разных вычислительных системах, а возможно (и даже иногда необходимо) наличие нескольких исполнений в пределах одного приложения. Поэтому со стороны исполнения мощность указана как 0..n.

Рисунок 12 Диаграмма отношений понятия "Исполнение"

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

Рисунок 13 Иллюстрация понятия области данных

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

Рисунок 14 Место абстракции "Программное представление" в модели

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

Наконец, собрав все наработки, описанные выше, воедино, получим Модель рабочей модели теперь уже произвольной предметной области (Рисунок 15), записанную, кстати, в канонической форме.

Рисунок 15 Модель рабочей модели произвольной предметной области

На этой диаграмме присутствуют некоторые связи, которые не были описаны ранее, в частичных диаграммах.

  1. Связь между программным классом и программным представлением. Указывает на то, что любое программное представление, даже не находящееся в оперативной памяти, должно иметь определенную структуру и поведение (которые определяются классом). Об этом уже было сказано в описании термина «Запись»;
  2. Ссылки классов (программных и предметной области) символизируют наследование классов.

Заключение

Сложность построения информационных систем на основе объектно-ориентированных технологий обусловлена расхождением идеологии элементов ООП, рассматриваемых в методиках объектно-ориентированного проектирования и средств, предоставляемых современными системами программирования. Это несоответствие проявляется в том, что в методиках объектно-ориентированного проектирования рассматривается процесс построения канонической модели предметной области, а использование распространенных в настоящее время средств разработки предполагает наличие рабочей модели, которая может быть на порядок сложнее канонической.