Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 78
Текст из файла (страница 78)
Права доступа пользователей к информации можноопределять на основе виртуальных, а не реальных таблиц, позволяя такимобразом записывать безопасные транзакции. Представления несколькоотличаются от таблиц, хотя бы тем, что связи в представлениях не могут бытьобновлены напрямую.В нашей системе SQL-эапросы будут играть роль абстракций низкогоуровня. Пользователи вряд ли будут разбираться в SQL, ведь этот язык неявляется частью предметной области. Мы будем использовать SQL приреализации программы. Составлять свои SQL-предложения смогут толькодостаточно искушенные в программировании разработчикиинструментальных средств нашей системы.
От простых смертных,работающих с системой каждый день, язык запросов будет скрыт.Рассмотрим следующую задачу: получив заказ, мы хотим определитьимя сделавшей его компании. С точки зрения программиста SQL, этонетрудная задача. Однако, в нашем случае, когда основное программированиевыполняется на C++, мы предпочли бы использовать следующее выражение:currentOrder.customer().name()С точки зрения объектно-ориентированного подхода это выражениевызывает селектор customer, возвращающий ссылку на клиента, а затем селектор name, возвращающий имя клиента.
На самом деле данное выражениевычисляется следующим запросом:SELECT NAMEFROMORDERS, CUSTOMERSWHERE ORDERS.CUSTOMERID CURRENTORDER.CUSTOMERIDANDORDERS . CUSTOMERID = CUSTOMERS .CUSTOMERIDСпрятав от клиента детали реализации данного вызова, мы скрыли отнего все неприятные особенности работы с SQL.Отображение объектно-ориентированного представления мира вреляционное концептуально ясно, но обычно требует довольно утомительнойпроработки деталей 33 По замечанию Румбаха, "Соединение объектноймодели с реляционной базой данных - в целом довольно простая задача, заисключением вопросов, связанных с обобщением" [16].
Румбах предлагаеттакже некоторые правила, которые следует учитывать при отображенииклассов и ассоциаций (включая агрегацию) на таблицы:• Каждый класс отображается в одну или несколько таблиц.• Каждое отношение "многие ко многим" отображается в отдельнуютаблицу.• Каждое отношение "один ко многим" отображается в отдельнуютаблицу или соотносится с внешним ключом [17].Далее он предлагает три альтернативных варианта отображенияиерархии наследования в таблицы:• Суперкласс и каждый его подкласс отображаются в таблицу.• Атрибуты суперкласса реплицируются в каждой таблице (и каждыйподкласс отображается в отдельную таблицу).• Атрибуты всех подклассов переносятся на уровень суперкласса(таким образом мы имеем одну таблицу для всей иерархии наследования) [18].Нет ничего удивительного в том, что существуют определенныеограничения по использованию SQL в низкоуровневой реализации.34 Вчастности, этот язык поддерживает ограниченный набор типов данных, аименно, символы, строки фиксированной длины, целые числа и вещественныечисла с фиксированной и плавающей точкой.
Отдельные реализации иногдаумеют работать и с другими типами данных; однако представлениеинформации в виде графических элементов или строк произвольной длинынапрямую не поддерживается.Анализ схем данныхДэйт задается следующим вопросом: "Пусть дан набор данных,которые надо расположить в базе данных. Как определить подходящуюлогическую структуру для этих данных? Другими словами, как определитьсвязи и атрибуты? Это и есть задача проектирования базы данных" [19].Оказывается, что идентификация ключевых абстракций базы данных вомногом напоминает процесс идентификации классов и объектов.
По этойпричине мы начнем разработку системы складского учета сразу с объектноориентированного анализа, в процессе которого будет формироватьсяструктура базы данных, а не будем сперва браться за создание схемы базыданных, и затем выводить из нее объектную модель.Начнем с уже перечисленного нами списка основных абстракций.Применив к нему правила Румбаха, мы получим следующие таблицы базыданных (сначала перечислим те из них, которые соответствуют ролям групп,принимающих участие в работе системы):• CustomerTable• SupplierTable• OrderAgentTable• AccountantTable• ShippingAgentTable• StockPersonTable• RecetvingAgentTable• Flannel-TableЗатем следуют таблицы, отражающие классификацию продуктов и ихналичие на складе:• ProductTable• InventoryTableИ, наконец, мы вводим таблицы для документопотока:• OrderTable• PurchaseOrderTable• InvoiceTable• PackingOrderTable• StockOrderTable• ShippingLabelTableМы не создавали таблиц для классов Report и Transaction, - результатыанализа подсказывают, что объекты этих классов не нуждаются в хранении.На следующем этапе анализа можно в деталях определить составатрибутов всех перечисленных таблиц.
Наверно, нет смысла обсуждать настраницах этой книги данные вопросы; мы уже останавливались на наиболееинтересных свойствах этих абстракции (см. рис. 10-4), а оставшиеся атрибутыдают мало нового с точки зрения архитектуры системы.10.2. ПроектированиеФормулируя подходы к архитектуре системы складского учета, мыдолжны помнить о трех моментах организационного характера: разделениефункций между клиентской и серверной частью, механизм управлениятранзакциями, стратегия реализации клиентской части приложения.Архитектура клиент/серверНаиболее важным вопросом реализации архитектуры клиент/серверявляется не столько вопрос о том, где будет проведена граница между этимидвумя частями, сколько о том, как разумно произвести это разделение.Возвращаясь к первоосновам, ответ на этот вопрос нам известен: нужнососредоточиться на поведении каждой абстракции, основывающемся наанализе вариантов использования каждой сущности, и только затем принятьрешение о размещении поведения.
После того, как мы проделаем такуюработу в отношении нескольких основных объектов, станут ясны общиемеханизмы, понимание которых поможет нам правильно разместитьоставшиеся абстракции.Для примера рассмотрим поведение классов Order и ProductRecord.Анализ первого из них дает нам следующий перечень необходимых операций:• construct• setCustomer• setOrderAgent• addItem• removeItem• orderID• customer• orderAgent• numberOfItems• itemAt• quantityOf• totalValueПеречисленные сервисные операции можно сразу выразить на языкеC++, предварительно дав два новых определения типов:// типы идентификационных номеров typedef unsigned int OrderID;// тип, описывающий местную валюту typedef float Money;Теперь получаем следующее определение класса:class Order { public:Order();Order(OrderID);Orderfconst Order&);-Order() ;Orders operator=(const Orders);int operator==(const Orders) const;int operator!=(const Orders) const;void setCustomer(Customer&);void setOrderAgent(OrderAgent&);void addItem(Product&, unsigned int quantity = 1);void removeItem(unsigned int index, unsigned int quantity = 1);OrderID orderID() const;Customer& customer() const;OrderAgent& orderAgent() const;unsigned int numberOfItem() const;Product& itemAt (unsigned int) const;unsigned int quantityOf(unsigned int) const;Money totalValue() const;protected:...};Обратим внимание на наличие нескольких вариантов конструктора.Первый из них используется по умолчанию (Order ()) для создания объекта сновым уникальным значением идентификатора OrderID.
Копирующийконструктор также создает объект с уникальным идентификатором, но приэтом копирует в него состояние объекта, использованного в качествеаргумента.Последний конструктор принимает в качестве аргумента OrderID, тоесть конструирует объект уже существующий в базе данных и извлекает избазы его параметры. Другими словами, в этом случае мы повторноматериализуем объект, существующий в базе данных. Такая операция,безусловно, требует выполнения некоторых действий: при восстановленииобъекта из базы данных соответствующий SQL-механизм должен либосделать объект разделяемым, либо синхронизировать состояние двухобъектов, созданных в разных приложениях.
Детали, конечно, скрыты вреализации и недоступны клиенту, который использует объект, применяяобычный объектный интерфейс.Реализация описанного подхода не вызывает особых затруднений.Если класс order спроектирован так, что его состояние полностьюопределяется идентификатором OrderID, то реализация операций сводится кобычным операторам чтения и записи из базы данных. Копии объектовсинхронизируются, поскольку соответствующая таблица в базе служитединым репозиторием состояния для всех представлений одного объекта.Диаграмма объектов на рис. 10-6 иллюстрирует описанный SQLмеханизм на примере сценария выставления счета. В сценарии реализованыследующие события:* aClient активизирует операцию setCustomer применительно к объектукласса Order; объект класса Customer передается в качестве параметра.• Объект класса Order вызывает селектор customerID c параметромзаказчика, позволяющим получить из базы данных соответствующийпервичный ключ.Рис.
10-6. Выставление счета• Объект, соответствующий заказу, использует SQL-операторUPDATE, чтобы установить идентификатор заказчика в базе данных заказов.Описанный механизм предполагает, что мы можем положиться насуществующий в базе данных механизм блокировки записей и взаимногоисключения при доступе (представьте себе, что могло бы случиться приодновременном обновлении одной записи из двух приложений). Если этотмеханизм блокировки должен быть видимым для клиента, то можновоспользоваться тем же подходом, который использовался нами при созданиибиблиотеки классов в главе 9.
Ниже мы покажем, что механизм выполнениятранзакции позволяет модифицировать за один прием несколько записей вбазе, обеспечивая тем самым целостность базы данных.После реализации описанного механизма вопрос о размещении бизнеслогики имеет скорее тактическое значение. В этом смысле ситуация неотличается от той.
которую мы имели бы в не объектно-ориентированнойархитектуре. Но в объектно-ориентированной архитектуре можно изменитьэти решения, скрыв сам факт изменения от клиента. Таким образом, клиентане затрагивают изменения, которые мы делаем по ходу настройки системы.Для примера рассмотрим два различных случая. Добавление в базуданных записей о наличии продуктов или удаление их, очевидно, должносогласовываться с бизнес-логикой, поэтому кажется естественным разместитьбизнес-логику на сервере. Добавление сведении о новых продуктах в базуданных требует точного их определения и однозначной идентификации.Кроме того, эти данные необходимо сделать доступными для всех клиентов,чтобы обновить их кэшированные таблицы. Удаление продукта из базы такжетребует проверки на наличие заказов по этому продукту и предупреждениясоответствующих клиентов.35Напротив, расчет стоимости заказов является более локальнойоперацией и его лучше выполнять в клиентской части приложения.
Выполняяподсчеты, мы запрашиваем в базе данных расцепки на все элементы заказа,складываем их, пересчитываем в нужную валюту, проверяем на допустимыеусловия кредитования и т. д.Итак, при выборе размещения функции в архитектуре клиент/сервермы следуем двум правилам: во-первых, реализовывать бизнес-правила иалгоритмы там где сосредоточена необходимая информация; во-вторых,размещать эти алгоритмы в нижних слоях объектно-ориентированнойархитектуры, чтобы внесение изменении не отражалось на системе в целом.Теперь вернемся к нашему примеру и рассмотрим более внимательнокласс product. Для этого класса мы определяем следующий набор операций:• construct• setDescription• setQuantity• setLocation• setSupplier• productID• description• quantity• location• supplierЭти операции являются общими для всех видов товаров.