Analysis Patterns 1.0 Help

14. Подходы для моделирования типов

Эта глава была написана в соавторстве с Джеймсом Оделлом.

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

В этой главе не делается попытка дать полный набор шаблонов проектирования для какой-либо конкретной среды реализации. Они слишком разные, каждая из них требует различных компромиссов. Это не просто вопрос Smalltalk или C++. Многие факторы — аппаратное обеспечение, базы данных, сети, библиотеки классов — влияют на шаблоны, которые фактически используются в проекте. Поэтому я сосредоточился на паттернах описанных в книге шаблоны проектирования — общих принципах и вопросах, которые следует учитывать при трансформации аналитических моделей в код.

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

Не все методы нуждаются в шаблонах проектирования. Если все моделирование выполняется с помощью метода, который глубоко укоренен в среде реализации, то преобразования практически не нужны. Это главное преимущество использования метода, основанного на реализации. Проблема такого подхода заключается в том, что существует большая дистанция между тем как люди думают о мире, и моделью, основанной на реализации. Также часто возникают проблемы с переносом этой модели в другую среду реализации. Шаблоны проектирования преследуют следующие цели:

  • Обеспечить, чтобы программное обеспечение было структурировано таким же образом, как и концептуальные модели, насколько это практически возможно

  • Обеспечение последовательности в программном обеспечении

  • Предоставить рекомендации по созданию программного обеспечения, чтобы знания эффективно распространялись по организации

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

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

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

В каждом разделе этой главы рассматривается ряд паттернов для преобразования концептуальных моделей. В начале мы рассмотрим паттерн для реализации ассоциаций (14.1). Существует три варианта реализации: указатели в обоих направлениях, указатели в одном направлении и объекты ассоциаций. Следуя базовому принципу, все они имеют одинаковый интерфейс. Фундаментальные типы имеют некоторые особые соображения. Ассоциации являются общими почти для всех методов, поэтому этот паттерн широко применим.

Во втором паттерне рассматривается реализация обобщения (14.2). Во многих методах обобщение рассматривается так же, как и наследование реализации. В этой книге используется множественная и динамическая классификация (см. раздел A.1.3), которые делают преобразование менее прямым. Мы рассматриваем пять реализаций: наследование, комбинированные классы с множественным наследованием, флаги, делегирование скрытому классу и создание замены. Мы снова определяем общий интерфейс, который включает операцию проверки типа объекта — такой тест следует использовать с осторожностью.

Остальные шаблоны короче и включают в себя шаблоны для создания объектов (14.3), уничтожения объектов (14.4), поиска объектов с точкой входа (14.5) и реализации ограничений (14.6). Мы кратко упомянем шаблоны проектирования для других техник (14.7), но подробного обсуждения не будет.

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

В разных языках существуют разные названия для различных элементов. Я использую термин поле для обозначения значения данных класса (переменная экземпляра Smalltalk или член данных C++). Операция — для обозначения сообщения, которое распознает класс (метод Smalltalk, или селектор, или функция-член C++). Я различаю операцию (объявление) и метод (тело); таким образом, полиморфная операция имеет много методов. Я использую термин характеристика для обозначения как поля, так и операции.

В этой главе предполагается, что у вас есть доступ к библиотеке классов, содержащей классы коллекций. Коллекции, также известные как контейнеры (библиотеки) — это классы, в которых хранится группа объектов. В обычных языках программирования наиболее распространенной и обычно единственной коллекцией является массив. Объектные среды могут предоставлять множество коллекций. Льюис [5] дает отличный обзор наиболее распространенных коллекций Smalltalk. Во многих версиях C++ используются аналогичные подходы, хотя они будут вытеснены стандартной библиотекой шаблонов (STL) [7]. К таким коллекциям относятся множества (неупорядоченные, без дубликатов), списки (упорядоченные коллекции в Smalltalk, векторы и деки в STL), пакеты (как множества, но с дубликатами, мультисеты в STL) и словари (map в STL). Словарь — это таблица поиска или ассоциативный массив, который позволяет искать объект, используя другой объект в качестве ключа. Например, у нас может быть словарь людей, проиндексированных по имени. Вы можете найти меня, отправив сообщение вида PeopleDictionary по адресу ("Martin Fowler").

Такие коллекции значительно упрощают программирование, и их наличие — одно из главных преимуществ объектно-ориентированной среды. Многие среды, включая все версии Smalltalk, поставляются с такой библиотекой классов. Большинство сред C++ не поставляются с классами коллекций, хотя их можно легко приобрести у многих поставщиков. Я настоятельно рекомендую вам ознакомиться с классами коллекций и использовать их. Работать в объектно-ориентированной среде и не использовать классы коллекций — все равно что программировать с одной рукой за спиной.

14.1 Реализация ассоциаций

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

Ряд практиков объектно-ориентированного подхода испытывают дискомфорт от использования ассоциаций в ОО-анализе. Они считают, что ассоциации нарушают принцип инкапсуляции в ОО-программировании. При инкапсуляции структура данных класса скрывается за интерфейсом операций. Некоторые практики считают, что ассоциации делают структуру данных общедоступной. Выходом из этой дилеммы является понимание того, как ассоциации интерпретируются в контексте ОО-языков. Ассоциации присутствуют, потому что они полезны в концептуальном моделировании. Они не противоречат инкапсуляции, если рассматривать их как способ описания того, что один тип объекта обязан отслеживать и изменять свои отношения с другим. Так, пример на рис. 14.1 показывает, что работник обязан знать своего работодателя и иметь возможность изменить его. И наоборот, организация несет ответственность за знание своих сотрудников и возможность их смены. В большинстве ОО-языков эта ответственность реализуется с помощью операций доступа и модификаторов (get и set). Конечно, структура данных может присутствовать, и в большинстве случаев она будет присутствовать, но структура данных не задается концептуальной моделью.

14 1

Рисунок 14.1. Пример ассоциации.

Атрибуты могут быть представлены в виде однозначных сопоставлений, обычно на фундаментальные типы. Таким образом, обсуждение однозначных отображений также применимо к атрибутам для тех методов, которые их используют.

14.1.1 Двунаправленные и однонаправленные ассоциации

Один из первых вопросов, который нам нужно решить — использовать двунаправленную или однонаправленную ассоциацию. На эту тему существует много споров. Однонаправленные ассоциации проще в реализации и вызывают меньшую связанность в программном обеспечении. Однако они усложняют поиск пути. В паттернах, описанных в этой книге, используются двунаправленные ассоциации. Мы можем выбрать: реализовать все ассоциации как двунаправленные, все как однонаправленные или использовать смесь. Использование смеси менее последовательно, но имеет свои преимущества. Если мы используем все двунаправленные ассоциации, мы можем столкнуться с проблемами сопряжения. Если использовать все однонаправленные ассоциации, то можно обнаружить некоторые ассоциации, которые действительно должны быть двунаправленными, и для них стоит сделать исключение.

Если мы хотим использовать однонаправленную ассоциацию, нам нужно решить, какое направление поддерживать, а какое отбросить. Это будет подсказано приложением. Хорошее эмпирическое правило — посмотреть, что хотят сделать клиенты ассоциации, и следовать тому направлению, которое им нужно. Я не верю в детальный анализ путей доступа в стиле многих методологий. Сначала мы должны сделать самую простую вещь, но быть готовыми изменить ее, если наши потребности изменятся позже. Если мы сохраняем модель, мы должны обновлять ее, чтобы показать, какое направление мы используем.

Если мы используем двунаправленные ассоциации, мы должны быть очень осторожны с теми, которые пересекаются с пакетами. Если мы сохраним двунаправленность, мы создадим взаимную видимость между категориями, как обсуждалось в разделе 11.2. Когда я использую двунаправленные ассоциации, я свободно применяю их внутри категории, но стараюсь избегать их между категориями, потому что важнее уменьшить видимость между категориями.

14.1.2 Интерфейс для ассоциаций

Интерфейс для ассоциаций в ОО-языке представляет собой ряд операций для доступа и обновления ассоциации. Точные условия и структура этих операций зависят от кардинальности соответствующих отображений.

В общем случае однозначное отображение требует двух операций: аксессора и модификатора. Аксессор не принимает никаких аргументов и возвращает объект, на который он указывает. Модификатор принимает один аргумент и изменяет сопоставление аргумента на целевой объект. Возможны различные соглашения об именовании. В Smalltalk принято называть обе операции mappingName, а модификатор отличается от аксессора наличием аргумента. Таким образом, на рис. 14.1 класс employee будет иметь две операции: employer и employer: anOrganization. В C++ не существует стандартной конвенции, но часто встречаются такие имена, как getEmployer() и setEmployer(Organization org). Использование getEmployer() и setEmployer() является наиболее естественным, но некоторые предпочитают использовать employerSet и employerGet() (или employerOf() и employerIs()), чтобы обе операции отображались вместе в браузере с алфавитной сортировкой.

Многозначное отображение требует трех операций. Снова есть аксессор, но он возвращает набор объектов. Предполагается, что все многозначные отображения являются множествами, если не указано иное. Интерфейс для не-множеств отличается и выходит за рамки этого раздела. Требуется два модификатора: один для добавления объекта, другой для его удаления. Аксессор обычно называется так же, как и однозначные отображения, только я предпочитаю форму множественного числа, чтобы еще больше усилить его многозначную природу (например, employees или getEmployees()). Модификаторы имеют форму addEmployee(Employee emp), removeEmployee(Employee emp) или employeesAdd: anEmployee, employeesRemove: anEmployee.

Не обязательно предоставлять модификаторы с обеих сторон двунаправленной ассоциации. Часто кажется, что модификаторы могут использоваться только в одном направлении, обычно в том, которое наиболее ограничено (например, Employee::employer). Аксессоры всегда должны предоставляться в обоих направлениях двунаправленной ассоциации; именно это и делает ее двунаправленной.

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

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

Проверка типов может быть выполнена в модификаторах, если она не встроена в язык. Это спорный вопрос в Smalltalk, который по своей природе является нетипизированным. Для проверки типов вам нужна некоторая возможность проверки типов, например, та, что обсуждается в разделе 14.2.6. Мне нравится помещать проверку типов в специальный блок предусловий. Все объекты имеют операцию require: aBlock. Эта операция оценивает блок и вызывает исключение, если результат false. Затем я проверяю тип в этом пункте с помощью утверждения типа self require: [aCustomer hasType: Customer]. Это позволяет мне легко убрать проверку типа по соображениям производительности, подобно проверке предусловий в Eiffel. (На самом деле я вообще использую эту структуру для проверки предусловий).

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

В некоторых случаях возврат множества многозначным аксессором может оказаться неэффективным. В таких случаях интерфейс можно расширить, включив в него общие операции над множествами (такие, как: select, do и collect) и итератор. Такие расширения должны соответствовать соглашениям об именовании того класса множеств, который вы используете. Однако такие расширения интерфейса могут привести к его раздуванию.

В C++ часто возникает вопрос о том, что должно быть возвращено аксессорами: объект или указатель на объект. То, что возвращается, должно быть явно указано в шаблонах проектирования. Общепринятой конвенцией является возвращение значения для всех встроенных типов данных, объекта для всех фундаментальных классов и указателя для всех остальных классов. В Smalltalk это неприменимо, поскольку вы всегда работаете с объектами, или, по крайней мере, так кажется! В нижеследующем обсуждении я всегда ссылаюсь на возврат ссылок; в реальных шаблонах должно быть ясно, что именно возвращается в C++ и подобных языках с явным указателем.

14.1.3 Фундаментальные типы

Некоторые типы объектов довольно просты и распространены во всех частях модели. Поэтому они требуют несколько иного обращения, чем большинство объектных типов, особенно в отношении ассоциаций. Примерами таких объектных типов являются классические встроенные типы данных сред программирования: integer, real, string и date. Однако хороший ОО анализ обычно обнаруживает и другие примеры: количество, деньги, период времени и валюта — типичные примеры. Трудно определить, что делает тип фундаментальным — в основном это происходит из-за его присутствия во всей модели и определенной внутренней простоты. Это означает, что если ассоциации фундаментального типа реализованы стандартным образом, то он будет обременен большим количеством операций, связывающих фундаментальные типы с другими типами по всей модели. Таким образом, в фундаментальных типах не должны быть реализованы сопоставления на нефундаментальные типы, то есть не должно быть никаких операций. Кроме того, ассоциации с другими фундаментальными типами должны обрабатываться в каждом конкретном случае.

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

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

14.1.4 Реализация однонаправленной ассоциации

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

14.1.5 Двунаправленная реализация с указателями в обоих направлениях

В этой реализации ассоциация реализуется указателями из обоих участвующих классов. Если отображение однозначное, то существует простой указатель от одного объекта к другому, как, например, указатель от Peter к NASA, показанный на рис. 14.2. Если отображение многозначное, то объект будет иметь набор указателей на другие объекты (например, на рис. 14.2 NASA указывает на набор указателей, который содержит указатели на Peter, Jasper и Paul). В языках, поддерживающих сдерживание (containment), может быть полезно содержать набор указателей, а не указывать на него. Однако это может иметь последствия для памяти приложения, поскольку множества могут произвольно расти. Аналогично, если однозначное отображение указывает на встроенный тип данных или на другой фундаментальный тип, то вместо указателя можно использовать сдерживание.

14 2

Рисунок 14.2. Реализация с помощью указателей в обоих направлениях.

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

Поскольку каждая связь между двумя объектами реализуется двумя указателями, важно, чтобы модификаторы поддерживали их синхронизацию. Таким образом, модификатор, вызываемый для изменения организации Питера на IBM, должен не только заменить указатель на Питера на указатель на IBM, но и удалить указатель на Питера в наборе сотрудников NASA и создать указатель в наборе сотрудников IBM. Но, сделав это, мы попадаем в OO-тупик. Employee должен использовать некоторую операцию, которая будет манипулировать только указателем на набор, не возвращая вызов Peter (иначе мы попадем в бесконечный цикл). Однако эта операция не должна быть частью интерфейса организации. В C++ это классическое использование конструкции friend. В Smalltalk мы должны создать такую операцию, но пометить ее как приватную (что, конечно, не мешает сотруднику использовать ее). В таких случаях полезно, чтобы только один модификатор выполнял фактическую работу, манипулируя данными и/или приватными операциями. Другой модификатор должен просто вызывать этот модификатор. Это гарантирует, что существует только одна копия кода обновления.

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

14.1.6 Двунаправленная реализация с указателями в одном направлении

Эта реализация использует указатели только в одном направлении. Чтобы перейти в другом направлении, нам нужно просмотреть все экземпляры класса и выбрать те, которые указывают на исходный объект. На рис. 14.3 для сопоставления с employee потребуется получить все экземпляры employee и выбрать те, чьим работодателем является NASA.

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

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

14 3

Рисунок 14.3. Реализация с указателями в одном направлении.

14.1.7 Двунаправленная реализация с помощью объектов ассоциации

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

14 4

Рисунок 14.4. Реализация с объектами ассоциации.

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

14.1.8 Сравнение двунаправленных реализаций

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

Ассоциативные объекты полезны в особых случаях, но в целом они не являются первым местом, где стоит искать.

14.1.9 Производные сопоставления

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

14.1.10 Сопоставления без использования класса множества

Хотя большинство многозначных сопоставлений — это множества, есть и исключения. В этой книге они обозначаются короткими семантическими формулировками, такими как [список], [иерархия] и [ключ: mappingName]. Эти типы выражений подразумевают другой интерфейс. Отображения, помеченные [list], возвращают список, а не множество, и имеют такие модификаторы, как addFirst, addLast, addBefore (Object) и indexOf(anObject). В этой книге я не пытался предоставить все интерфейсы для этих случаев. Однако если мы используем такие конструкции, нам следует позаботиться о разработке шаблонов проектирования для них. Обычно мы должны основывать интерфейс на интерфейсе базовой коллекции. Мы также можем рассматривать эти конструкции как шаблоны ассоциаций (см. главу 15).

14.2 Реализация обобщения

Одно из наиболее заметных отличий моделирования OO типа от большинства обычных методов моделирования данных — широкое использование обобщения. Хотя обобщение уже давно является частью многих подходов к моделированию данных, оно часто рассматривается как продвинутая или специализированная техника. Тесная связь между обобщением и наследованием в ОО обеспечивает ему центральное место в OO анализе.

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

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

14.2.1 Реализация путем наследования

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

14.2.2 Реализация с помощью комбинированных классов с множественным наследованием

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

14 5

Рисунок 14.5 Пример множественной классификации.

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

14.2.3 Реализация с помощью флагов

Если бы программиста, никогда не слышавшего о наследовании, спросили, как реализовать потребность в том, чтобы клиенты фиксировали, являются ли они приоритетными или нет, ответ, вероятно, был бы «с помощью флага состояния». Эта старомодная схема по-прежнему эффективна. Она обеспечивает быстрый способ поддержки множественной и динамической классификации. Флаги легко меняются по желанию, и для каждого раздела можно определить одно поле флага. Действительно, именно эта схема используется для изменения состояния в ОО-программах, не основанных на динамической классификации.

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

Если объект-получатель не является экземпляром подтипа, то, очевидно, не следует использовать операции, определенные для подтипа, такие как запрос повторения неприоритетного клиента, как показано на рис. 14.6. Это вызовет ошибку (ошибку времени выполнения в Smalltalk и, вероятно, ошибку времени компиляции в C++), если мы используем наследование. Все операции, определенные для подтипа, должны быть защищены проверкой, чтобы убедиться, что получатель относится к этому подтипу. Если проверка не удается, процедура завершается, выдавая какой-либо признак проблемы, обычно исключение. Это выявляет еще один недостаток данной схемы в C++ — невозможно отловить эти ошибки до момента компиляции.

14 6

Рисунок 14.6. Пример приоритетного клиента.

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

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

14.2.4 Реализация путем делегирования скрытому классу

Этот подход — полезный вариант использования флагов для реализации подтипизации. В этом случае для подтипа подготавливается класс, но этот класс скрыт от всех, кроме класса супертипа. Мы должны предоставить поле в классе супертипа для ссылки на подтип (которая может удваиваться как флаг). И снова мы должны перенести все операции подтипа в интерфейс супертипа. Однако структура данных остается в супертипе. Все операции над классом супертипа, которые исходят от класса подтипа, делегируют вызов классу подтипа, в котором находится фактический метод.

Таким образом, для концептуальной модели, показанной на рис. 14.7, экземпляр executive будет иметь по одному экземпляру employee и executive, как показано на рис. 14.8. Объект executive и его класс не видны ни одному компоненту, кроме класса employee. (В C++ все его члены были бы private, а employee — friend). Операция giveStock, определенная для типа executive, будет помещена на employee. Когда объекту employee со связанным с ним исполнителем отправляется зарплата, метод employee для получения зарплаты просто вызывает метод pay() у executive и возвращает любой результат. Таким образом, никакая другая часть системы не знает, как реализована подтипизация. Выбор метода для полиморфных операций реализуется так же, как и для флагов (внутренний оператор case), с вызовом метода исполнителя, если это необходимо. Другим подходом было бы поместить все методы на employee и сделать executive не более чем структурой данных. Однако это сделает executive менее самодостаточным модулем.

14 7

Рисунок 14.7. Концептуальная модель сотрудника и руководителя.

14 8

Рисунок 14.8. Модель реализации рис. 14.7 с использованием делегирования скрытому классу.

Логическим завершением этого подхода является шаблон состояния, показанный на рис. 14.9. В этом случае всегда присутствует скрытый класс. Все различные скрытые классы имеют общий абстрактный суперкласс, который сам является скрытым. Employee просто делегирует оплату своему скрытому классу. Какой бы подкласс ни присутствовал, он реагирует соответствующим образом. Это позволяет добавлять новые подтипы без изменения класса employee, если они не вносят изменений в интерфейс employee (аналогичный подход используется в идиоме envelope/letter [3]).

14 9

Экземпляр абстрактного класса EmployeeGrade присутствует всегда. Любое поведение, зависящее от состояния, объявляется в EmployeeGrade как абстрактный метод и реализуется подклассами. Я использовал стрелку, чтобы показать подклассификацию (из Унифицированного языка моделирования (UML) компании Rational [1]), чтобы усилить разницу между подклассификацией и подтипизацией.

Рисунок 14.9 Реализация сотрудников и руководителей с помощью шаблона state.

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

14.2.5 Реализация путем создания замены

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

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

Во многих средах самая большая проблема — найти все ссылки на старый объект и перенести их в новый. Без управления памятью это может быть практически невозможно. Любые не пойманные ссылки становятся висячими указателями и приводят к аварийному завершению работы, которое трудно отладить. Поэтому такой подход не рекомендуется использовать в C++, если только не используется какая-то схема управления памятью, которая может надежно найти все ссылки. Языки с управлением памятью могут найти это проще; Smalltalk предоставляет метод (become) для выполнения подкачки ссылок.

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

14.2.6 Интерфейс для обобщения

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

Спорный вопрос в OO-программировании — должна ли существовать операция, возвращающая классификацию объекта. Такая операция часто важна — как еще мы можем взять набор людей и отфильтровать его, чтобы оставить только женщин? Однако такая операция таит в себе опасность того, что программисты будут использовать ее в рамках case-описания, подрывая полиморфизм и те преимущества, которые он дает. Кажется, в рамках структуры ОО-программирования мало что можно сделать для устранения этой дилеммы. Операция возврата классификации объекта часто бывает необходима, и поэтому ее следует предусмотреть. Однако ради хорошего стиля программирования мы не должны использовать такую операцию вместо полиморфизма. Как правило, информация о классификации должна запрашиваться только в рамках чистого сбора информации в запросе или для отображения интерфейса.

В настоящее время существуют некоторые соглашения для определения классификации объекта. И программисты Smalltalk, и программисты C++ используют операции с именем isState-Name, чтобы определить, находится ли объект в определенном состоянии. В Smalltalk есть сообщение isKindOf: aClass для определения принадлежности к классу. В C++ информация о классе не хранится во время выполнения (хотя это изменится с выходом нового стандарта). Однако иногда операции, которые эффективно предоставляют эту информацию, предоставляются, когда в этом есть необходимость.

Можно использовать две широкие схемы именования. Первая заключается в использовании формы именования isTypeName. Вторая — предоставление параметрической операции, такой как hasType(TypeName). Первая схема — это обычная конвенция, используемая с флагами и скрытыми классами. Она хорошо работает в таком виде, но имеет проблему с подклассификацией. Если мы хотим добавить новый подкласс к существующему классу, нам нужно добавить операцию isTypeName как к суперклассу, так и к подклассу. В противном случае вызов isTypeName для суперкласса приводит к ошибке. Соглашение hasType является более расширяемым, поскольку подклассы можно добавлять без изменения суперкласса. Помните, что во всех случаях нам нужна информация о типе, а не о классе.

Для изменения типов не существует типичного стандарта именования. Разумны такие названия, как make-TypeName или classifyAsTypeName (я предпочитаю первое). Такие операции должны отвечать за рассекречивание от любых несовпадающих типов. Таким образом, полный раздел должен иметь только столько модификаторов, сколько типов в разделе. Неполные разделы должны каким-то образом переходить в неполное состояние. Это можно сделать либо предоставив методы declassifyAsTypeName для каждого типа объектов в разделе, либо предоставив единственную операцию declassifyIn-PartitionName. Обратите внимание, что разделы, которые, как ожидается, не будут динамическими, не будут иметь этих модификаторов.

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

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

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

14.2.7 Реализация операции hasType

На этом этапе полезно сказать несколько слов о реализации аксессора типов. Каждому классу в системе потребуется операция hasType. Метод будет проверять аргумент на соответствие всем типам, реализуемым классом. Если использовались флаги, то они проверяются для проверки типа. Даже если флагов нет, класс почти наверняка реализует определенный тип, и этот тип должен быть проверен. Если любой из этих тестов верен, то возвращается true. Если же ни один из типов класса не совпадает, то необходимо вызвать метод суперкласса и вернуть его результат. Если суперкласса нет, то возвращается false. Таким образом, на практике сообщение, отправленное в нижнюю часть иерархии, будет подниматься вверх по иерархии до тех пор, пока не найдет совпадение или не закончится в верхней части и не вернется false. Этот механизм позволяет легко расширять иерархию типов, поскольку проверять тип нужно только в классе, который его реализует.

14.3 Создание объектов

Механизмы необходимы для создания новых объектов, как тех, которые реализуются классом напрямую, так и тех, которые реализуются косвенно.

14.3.1 Интерфейс для создания

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

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

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

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

14.3.2 Реализация для создания

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

В Smalltalk обычная идиома заключается в том, что каждый класс поддерживает сообщение о создании (часто называемое new), которое может принимать аргументы. Во время создания нового объекта часто посылается сообщение initialize, которое не принимает аргументов. Этот initialize полезен для установки переменных экземпляра многозначных отображений в новый набор, но не может поддерживать инициализацию ассоциаций, поскольку не принимает аргументов. Лучше всего использовать паттерн Creation Parameter Method Кента Бека [1], имея специальный метод для установки этих начальных параметров.

C++ предоставляет конструктор для инициализации. Здесь можно сделать многое, но иногда могут возникнуть проблемы с семантикой конструктора. Часто лучше использовать конструктор только в рамках другой операции create; паттерны создания «банды четырех» [4] особенно полезны для таких случаев.

14.4 Уничтожение объектов

Как объекты живут, так они могут и умереть. Не все объекты можно уничтожить, некоторые должны жить вечно (например, медицинские карты). Но даже в этом случае они могут быть уничтожены одной системой после того, как были заархивированы в другом месте. Самая большая проблема при уничтожении объектов — это преодоление последствий. Например, удаление экземпляра order с рис. 14.10 вызывает проблему, если с ним связаны какие-либо линии order. Такие линии заказа должны иметь заказ (обязательная ассоциация), поэтому если мы просто удалим заказ, линии заказа нарушат свои ограничения.

14 10

Рисунок 14.10. Пример с клиентами и заказами.

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

На практике удаление может иметь различную степень каскадности. Операция уничтожения может быть множественной по отношению к одним отображениям и единичной по отношению к другим. Это вполне допустимо, но необходимо убедиться, что уничтожение происходит по принципу «все или ничего».

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

14.4.1 Интерфейс для уничтожения

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

14.4.2 Реализация для уничтожения

Именно во время уничтожения присутствие управления памятью становится наиболее ощутимым. Оно мало что меняет в самом методе уничтожения, но влияет на последствия ошибок.

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

14.5 Точка входа

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

Нам не нужен список всех экземпляров для всех типов. Рассмотрим пример на рис. 14.11. Поскольку все экземпляры линии заказа связаны с экземпляром заказа, нам не нужно хранить ссылку от типа линия заказа на все его экземпляры. Если мы считаем, что редко кто будет запрашивать все строки заказа, независимо от заказа или товара, то мы можем пренебречь этой ссылкой. В том случае, если кто-то все-таки захочет получить список всех строк заказа, мы можем предоставить его, получив список всех экземпляров типа order и перейдя через отображение к строке order. Таким образом, мы можем сэкономить место, необходимое для хранения всех ссылок на все экземпляры строки заказа, ценой одного уровня непрямой связи, если нам когда-нибудь понадобятся все экземпляры строки заказа. Это чисто реализационный компромисс. В реляционной базе данных этот компромисс не имеет значения, поскольку в базе данных используются фиксированные таблицы.

14 11

Рисунок 14.11. Пример клиента, заказа, продукта.

Этот же аргумент можно распространить и на заказ. Можно считать, что все экземпляры order необходимы, если человек хочет выбрать заказ, набрав номер заказа. Поскольку номер заказа обычно представляет собой строку, ссылки от строки к заказу обычно не хранятся, и все экземпляры заказа необходимы. Однако можно утверждать, что в действительности к заказам всегда обращаются после того, как найден клиент. И снова это вопрос реализации — хранить указатели или нет.

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

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

14.5.1 Интерфейс для поиска объектов

Для всех типов полезно иметь операцию, которая возвращает все экземпляры типа. Такая операция необходима для того, чтобы указатели в одном направлении работали при навигации против направления.

Часто бывает полезно предоставить некоторую операцию для поиска экземпляра по определенным критериям. Примером может быть findCustomer(customer-Number). Хотя трудно сформулировать общие правила использования такой операции, в целом наиболее естественным способом является использование навигации. Таким образом, вместо того чтобы просить найти все заказы, заказчиком которых является ABC, концептуально проще спросить заказчика ABC обо всех его заказах. Это может вызвать проблемы с оптимизацией из-за навигационного выражения запроса, но они часто могут быть решены в аксессоре заказчика.

Когда поиск ведется по фундаментальным типам, эта опция неприменима, поэтому полезнее использовать общую процедуру find. Однако даже в этом случае она должна быть выполнена как можно более общим способом. Самый простой подход — запросить все экземпляры класса, а затем использовать встроенную операцию select для возвращаемого набора. Это не очень хорошо работает для классов с большим количеством экземпляров. Следующий шаг — предоставить операцию select, которая будет принимать в качестве аргумента любую булеву операцию. Это обеспечивает максимальную гибкость при наличии только одной операции в интерфейсе класса. Однако в одних языках это сделать гораздо сложнее, чем в других. Только когда эти подходы исчерпаны и делать это более общим способом слишком дорого, следует использовать find с определенными аргументами. Мы всегда должны следить за тем, чтобы не раздувать интерфейс класса.

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

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

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

14.5.2 Реализация операций поиска

Обычный способ реализации точки входа — через некоторый класс коллекции. Эта коллекция может быть специальным классом-синглтоном (например, customer list) или статическим полем в классе. Запрос типа на его экземпляры означает, что возвращаются объекты коллекции. Как и в случае с многозначными ассоциациями, важно, чтобы коллекция была неизменяемой, кроме как через интерфейс точки входа. Точка входа, не являющаяся точкой входа, также обычно имеет операцию возврата всех экземпляров. Это можно сделать, перейдя из точки входа. Операции select и find работают аналогичным образом.

14.5.3 Использование классов или объектов-регистраторов

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

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

14.6 Реализация ограничений

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

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

Часто бывает полезно реализовать явный аксессор, чтобы определить, вписывается ли объект в его ограничения. Для всех классов должна быть предусмотрена операция с именем типа checkInvariant, чтобы генерировать исключение, если что-то не так, и ничего не делать, если все в порядке. Это может быть использовано в качестве проверки здоровья в различных точках, в том числе как часть проверки постусловий во время отладки и как часть проверки целостности системы во время работы — что особенно ценно для систем баз данных.

В Smalltalk и C++ нет явных возможностей для ограничений и утверждений, как в Eiffel. Их можно установить с помощью слабой, но достаточно эффективной альтернативы. В Smalltalk вы можете создать операцию (называемую что-то вроде require: aBlock), которая принимает блок в качестве аргумента. В объекте класса можно написать метод, который будет выполнять блок и выбрасывать исключение, если он окажется ложным. Метод require можно использовать для проверки предусловий, инвариантов и некоторых проверок постусловий. В C++ есть макрос assert, который можно использовать для тех же целей.

14.7 Шаблоны дизайна для других техник

В этой книге преобладают модели типов. Поэтому шаблоны проектирования в этой главе представляют собой преобразования из моделей типов. Аналогичные принципы могут применяться и в других техниках. Хотя такое прямое отображение не так правдоподобно, шаблоны проектирования могут быть предоставлены для диаграмм событий [6]. За последние несколько лет было довольно много дискуссий о шаблонах проектирования для различных видов моделей состояний, хотя мы все еще ждем солидного заявления по этому вопросу. Диаграммы взаимодействия достаточно близки к реализации, чтобы их связь с кодом была достаточно очевидной.

За последние несколько лет появилась небольшая, но значительная группа разработчиков, подчеркивающих важность такого подхода к трансформации. Шлаер и Меллор были в авангарде этой группы [8]. Я надеюсь, что со временем этой теме будет уделяться все больше внимания и мы увидим больше паттернов и несколько полноценных шаблонов проектирования. Я подозреваю, что полный набор шаблонов, скорее всего, будет создан либо как коммерческий инструмент (возможно, связанный с CASE-средствами), либо как собственная работа. Я надеюсь, что шаблоны для таких шаблонов станут регулярной частью литературы.

Ссылки

  1. Beck, K. Smalltalk Best Practice Patterns Volume 1: Coding, Englewood Cliffs, NJ: Prentice-Hall, in press.

  2. Booch, G., and J. Rumbaugh. Unified Method for Object-Oriented Development Rational Software Corporation, Version 0.8, 1995.

  3. Coplien, J.O. Advanced C++ Programming Styles and Idioms. Reading, MA: Addison-Wesley, 1992.

  4. Gamma, E., R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.

  5. Lewis, S. The Art and Science of Smalltalk. Hemel Hempstead, UK: Prentice-Hall International, 1995.

  6. Martin, J. and J.J. Odell. Object-Oriented Methods: Pragmatic Considerations. Englewood Cliffs, NJ: Prentice-Hall, 1996.

  7. Musser, D.R., and A. Saini. STL Tutorial and Reference Guide. Reading, MA: Addison-Wesley, 1996.

  8. Shlaer, S., and S.J. Mellor. A deeper look at the transition from analysis to design. Journal of Object-Oriented Programming, 5, 9 (1993), pp. 16–21.

Last modified: 16 January 2025