- •Оглавление
- •Предисловие
- •Введение
- •Часть I Обзор Глава 1 "Расслоение" системы
- •Развитие модели слоев в корпоративных программных приложениях
- •Три основных слоя
- •Где должны функционировать слои
- •Глава 2 Организация бизнес-логики
- •Выбор типового решения
- •Глава 3 Объектные модели и реляционные базы данных
- •Архитектурные решения
- •Функциональные проблемы
- •Считывание данных
- •Взаимное отображение объектов и реляционных структур
- •Отображение связей
- •Наследование
- •Реализация отображения
- •Двойное отображение
- •Использование метаданных
- •Соединение с базой данных
- •Другие проблемы
- •Дополнительные источники информации
- •Глава 4 Представление данных в Web
- •Типовые решения представлений
- •Типовые решения входных контроллеров
- •Дополнительные источники информации
- •Глава 5 Управление параллельными заданиями
- •Проблемы параллелизма
- •Контексты выполнения
- •Изолированность и устойчивость данных
- •Стратегии блокирования
- •Предотвращение возможности несогласованного чтения данных
- •Разрешение взаимоблокировок
- •Транзакции
- •Типовые решения задачи обеспечения автономного параллелизма
- •Параллельные операции и серверы приложений
- •Дополнительные источники информации
- •Глава 6 Сеансы и состояния
- •В чем преимущество отсутствия "состояния"
- •Состояние сеанса
- •Глава 7 Стратегии распределенных вычислений
- •Соблазны модели распределенных объектов
- •Интерфейсы локального и удаленного вызова
- •Когда без распределения не обойтись
- •Сужение границ распределения
- •Интерфейсы распределения
- •Глава 8 Общая картина
- •Предметная область
- •Источник данных
- •Платформы и инструменты
- •Другие модели слоев
- •Часть II Типовые решения Глава 9 Представление бизнес-логики Сценарий транзакции (Transaction Script)
- •Модель предметной области (Domain Model)
- •Модуль таблицы (Table Module)
- •Слой служб (Service Layer)
- •Глава 10 Архитектурные типовые решения источников данных Шлюз таблицы данных (Table Data Gateway)
- •Шлюз записи данных (Row Data Gateway)
- •Активная запись (Active Record)
- •Преобразователь данных (Data Mapper)
- •Глава 11 Объектно-реляционные типовые решения, предназначенные для моделирования поведения Единица работы (Unit of Work)
- •Коллекция объектов (Identity Map)
- •Загрузка по требованию (Lazy Load)
- •Глава 12 Объектно-реляционные типовые решения, предназначенные для моделирования структуры Поле идентификации (Identity Field)
- •Отображение внешних ключей (Foreign Key Mapping)
- •Отображение с помощью таблицы ассоциаций (Association Table Mapping)
- •Отображение зависимых объектов (Dependent Mapping)
- •Внедренное значение (Embedded Value)
- •Сериализованный крупный объект (Serialized lob)
- •Наследование с одной таблицей (Single Table Inheritance)
- •Наследование с таблицами для каждого класса (Class Table Inheritance)
- •Наследование с таблицами для каждого конкретного класса (Concrete Table Inheritance)
- •Преобразователи наследования (Inheritance Mappers)
- •Глава 13 Типовые решения объектно-реляционного отображения с использованием метаданных Отображение метаданных (Metadata Mapping)
- •Объект запроса (Query Object)
- •Хранилище (Repository)
- •Глава 14 Типовые решения, предназначенные для представления данных в Web Модель-представление-контроллер (Model View Controller)
- •Контроллер страниц (Page Controller)
- •Контроллер запросов (Front Controller)
- •Представление по шаблону (Template View)
- •Представление с преобразованием (Transform View)
- •Двухэтапное представление (Two Step View)
- •Контроллер приложения (Application Controller)
- •Глава 15 Типовые решения распределенной обработки данных Интерфейс удаленного доступа (Remote Facade)
- •Объект переноса данных (Data Transfer Object)
- •Глава 16 Типовые решения для обработки задач автономного параллелизма Оптимистическая автономная блокировка (Optimistic Offline Lock)
- •Пессимистическая автономная блокировка (Pessimistic Offline Lock)
- •Блокировка с низкой степенью детализации (Coarse-Grained Lock)
- •Неявная блокировка (Implicit Lock)
- •Глава 17 Типовые решения для хранения состояния сеанса Сохранение состояния сеанса на стороне клиента (Client Session State)
- •Сохранение состояния сеанса на стороне сервера (Server Session State)
- •Сохранение состояния сеанса в базе данных (Database Session State)
- •Глава 18 Базовые типовые решения Шлюз (Gateway)
- •Преобразователь (Mapper)
- •Супертип слоя (Layer Supertype)
- •Отделенный интерфейс (Separated Interface)
- •Реестр (Registry)
- •Объект-значение (Value Object)
- •Деньги (Money)
- •Частный случай (Special Case)
- •Дополнительный модуль (Plugin)
- •Фиктивная служба (Service Stub)
- •Множество записей (Record Set)
- •Список типовых решений
- •Шпаргалка
- •Как управлять сложным потоком функций приложения?
- •Как взаимодействовать с базой данных?
- •Как избежать загрузки в оперативную память всего содержимого базы данных?
- •Как сохранить структуры наследования в реляционной базе данных?
Реестр (Registry)
"Глобальный" объект, который используется другими объектами для поиска общих объектов или служб
Как следует поступить, чтобы найти какой-нибудь объект? Обычно мы обращаемся к другому объекту, который связан с искомым, и используем эту связь для перехода к последнему. Таким образом, если необходимо найти все заказы, сделанные заданным покупателем, следует обратиться к объекту покупателя и вызвать его метод для извлечения нужных заказов. К сожалению, иногда просто не с чего начать поиск: например, когда есть идентификатор покупателя, но нет ссылки на соответствующий объект. В этом случае понадобится какой-либо специальный метод поиска, однако куда же его поместить, чтобы он стал доступным из разных частей приложения?
Типовое решение реестр представляет собой глобальный объект, по крайней мере он выглядит как глобальный, даже если не является таковым в действительности.
Принцип действия
Проектирование реестра, как, впрочем, и любого другого объекта, должно рассматриваться в терминах интерфейса и реализации. Как и у всех других объектов, интерфейс и реализация реестра совершенно непохожи друг на друга, хотя многие почему-то думают, что они должны быть одинаковы.
В качестве интерфейса реестров рекомендую применять статические методы. Статический метод класса доступен всем объектам приложения. Более того, в статический метод можно поместить всю необходимую логику, включая делегирование полномочий другим методам, какими бы они не были — статическими или методами экземпляров объектов.
Эти идеи, однако, не означают, что данные реестра должны храниться в статических полях. Вообще говоря, я не использую статические поля, если только их значения не являются константами.
Прежде чем выбирать способ хранения данных реестра, подумайте об области их видимости. Эти данные могут фигурировать в различных контекстах выполнения. Одни из них являются глобальными по отношению ко всему процессу, другие — по отношению к потоку, а третьи и вовсе по отношению к сеансу. Разные области видимости требуют различных реализаций, однако интерфейс при этом может быть общий. Программист, пишущий код приложения, не должен знать, какие данные возвращает вызов статического метода — глобальные по отношению к процессу или по отношению к потоку. Для разных областей видимости можно предусмотреть разные реестры, но можно обойтись и одним, различные методы которого будут оперировать данными, имеющими разные области видимости.
Если определенные данные используются в контексте всего процесса, соответствующее поле реестра можно сделать статическим. Однако я редко использую статические поля для изменяемых данных, поскольку они не позволяют заменить реестр фиктивным объектом. Возможность замены реестра особенно важна в отношении тестирования (для этого можно воспользоваться дополнительным модулем (Plugin)).
Реализуя реестр, глобальный по отношению к процессу, рекомендуется применять типовое решение единственный элемент (Singleton) [20]. Последнее гарантирует, что в системе будет существовать только один экземпляр соответствующего класса. В этом случае класс реестра будет состоять из единственного статического поля, содержащего экземпляр реестра. Зачастую при использовании объекта единственный элемент разработчики явно обращаются к его содержимому (посредством методов наподобие Registry.soleInstance.getFoo()), однако я предпочитаю применять статический метод, который скрывает от меня наличие единственного экземпляра объекта (Registry.getFoo()). Это особенно подходит для языков программирования, созданных на основе С, поскольку в них статические методы могут обращаться к закрытым данным экземпляра объекта.
Объекты с единственным экземпляром хорошо применять в однопоточных приложениях, а вот в многопоточных они могут стать настоящей проблемой. Манипулирование одним и тем же объектом в нескольких параллельных потоках зачастую приводит к совершенно непредсказуемым результатам. В качестве решения данной проблемы следовало бы применить синхронизацию, однако трудность написания кода синхронизации может окончательно свести вас с ума, прежде чем вам удастся ликвидировать все спорные моменты. Поэтому я не рекомендую использовать типовое решение единственный элемент для сохранения изменяемых данных в многопоточном окружении. Напротив, объекты с единственным экземпляром хорошо подходят для хранения неизменяемых данных, потому что невозможность изменения данных исключает возникновение конфликтов между параллельными потоками. Идеальным содержимым реестра, глобального по отношению к процессу, было бы нечто наподобие списка штатов США. Такие данные могут быть загружены в самом начале процесса и никогда не требуют изменений. Если же изменения все-таки случаются, они так редки, что обновление соответствующих данных можно реализовать посредством какого-либо радикального подхода, например прерывания процесса.
В большинстве случаев данные реестра являются глобальными по отношению к потоку. В качестве примера можно привести соединение с базой данных. Для проведения сеанса работы с базой данных многие среды разработки предоставляют хранилища, специфичные по отношению к потоку, наподобие Java-классов ThreadLocal. Вместо этого для хранения данных может применяться словарь, индексированный по потоку, элементами которого являются соответствующие объекты данных. В этом случае запрос на получение соединения приводит к выполнению поиска по значению текущего потока.
Манипулируя данными, глобальными по отношению к потоку, следует помнить, что внешне они ничем не отличаются от данных, глобальных по отношению к процессу. Метод наподобие Registry.getDbConnection() будет иметь одинаковый вид и для тех и для других данных.
Поиск по словарю может применяться и для данных, глобальных по отношению к сеансу. Для работы с такими данными необходимо иметь идентификатор сеанса, который в начале выполнения запроса может быть помещен в реестр, глобальный по отношению к потоку. В этом случае для выполнения последующего доступа к данным сеанса объекты приложения могут проводить поиск в коллекции, индексированной по идентификаторам сеанса, используя значение идентификатора, хранящееся в реестре, глобальном по отношению к потоку.
Используя глобальный по отношению к потоку реестр со статическими методами, вы можете столкнуться с проблемами производительности, возникающими при попытках доступа к статическим методам реестра нескольких потоков. Избежать подобных проблем поможет прямое обращение к экземпляру потока.
Некоторым приложениям достаточно одного реестра, а некоторым может понадобиться сразу несколько. Как правило, реестры приложения группируются по слоям системы или же по контекстам выполнения. Я же предпочитаю группировать их по принципу использования, а не реализации.
Назначение
Несмотря на инкапсуляцию своих методов, содержимое реестра является глобальным по отношению к определенному контексту. Я никогда не любил работать с глобальными данными. В моих приложениях практически всегда встречается та или иная разновидность реестра, однако я совершенно искренне стараюсь избегать его применения и использую обычные переходы по связям между объектами там, где только возможно. Как правило, реестр следует применять только в случае крайней необходимости.
Существует несколько альтернатив использованию реестра. Одна из них состоит в том, чтобы передавать глобальные данные в виде параметров. К сожалению, этот подход приводит к тому, что параметры добавляются в вызовы методов, которым они совсем не нужны. Нередки ситуации, когда методы, действительно нуждающиеся в переданных параметрах, находятся в дереве вызовов на несколько слоев ниже тех, которым эти параметры были переданы. Передача практически ненужных параметров кажется мне пустой тратой времени, поэтому в подобных ситуациях я отдаю предпочтение реестру.
Еще одна альтернатива использованию реестра заключается в том, чтобы при создании экземпляров объектов добавлять к ним ссылки на необходимые глобальные данные. Разумеется, при этом в конструкторе объекта появляется лишний параметр, однако он по крайней мере не будет фигурировать в вызовах других методов. Как и предыдущий, этот подход имеет больше недостатков, чем преимуществ, но, если у вас есть глобальные данные, которые применяются только некоторым подмножеством классов, он может оказаться довольно полезным.
Существенным недостатком реестра является необходимость его изменения при добавлении новых объектов. Вследствие этого многие предпочитают хранить глобальные данные в коллекциях. Тем не менее я рекомендую использовать явные классы с явными методами, которые позволяют проследить, какие ключи применяются для поиска объекта. Чтобы понять принцип работы явного метода, достаточно взглянуть на исходный код или сгенерированную документацию. Использование коллекции лишено таких преимуществ. Чтобы понять, по какому ключу выполняется поиск, вам понадобится найти места системы, в которых происходит считывание или запись в коллекцию, либо обратиться к документации, которая, как известно, быстро становится неактуальной. Явный класс позволяет сохранить типовую безопасность в статически типизированных языках, а также инкапсулировать структуру реестра, чтобы при последующем росте системы ее можно было вынести в нужный класс или слой. Обычная коллекция не является инкапсулированной, что значительно усложняет сокрытие реализации. Это особенно неудобно, если область видимости данных требуется изменить.
Как видите, необходимость в применении реестра все же возникает. Не забывайте, однако, что глобальных данных следует избегать до тех пор, пока их присутствие не станет неизбежным.
Пример: реестр с единственным экземпляром (Java)
Представьте себе приложение, которое считывает данные из базы данных и затем вносит в них изменения, чтобы получить нужную информацию. Для упрощения задачи наша система будет осуществлять доступ к данным посредством шлюзов записи данных (Row Data Gateway). Логика запросов к базе данных будет инкапсулирована в специальных методах поиска. Последние лучше реализовать в виде методов экземпляров объектов, чтобы при тестировании их можно было заменить фиктивной службой (Service Stub). Объекты поиска нужно где-нибудь разместить и, очевидно, наиболее подходящим для этого является реестр.
Реестр с единственным экземпляром объекта представляет собой очень простой пример типового решения единственный элемент (Singleton) [20]. Единственный экземпляр объекта реестра хранится в статическом поле.
class Registry...
private static Registry getInstance() {
return soleInstance;
}
private static Registry soleInstance = new Registry();
Все, что хранится в реестре, помещается в его экземпляр.
class Registry...
protected PersonFinder personFinder = new PersonFinder();
Для облегчения глобального доступа открытые методы реестра сделаны статическими.
class Registry...
public static PersonFinder personFinder() {
return getInstance().personFinder;
}
Чтобы заново инициализировать реестр, достаточно еще раз создать его единственный экземпляр.
class Registry...
public static void initialize() {
soleInstance = new Registry();
}
Чтобы во время тестирования реестр можно было заменить фиктивной службой, я создам класс, производный от класса реестра.
class RegistryStub extends Registry...
public RegistryStub() {
personFinder = new PersonFinderStub();
}
Вместо реального обращения к базе данных метод поиска фиктивной службы будет возвращать некий стандартный экземпляр шлюза записи данных, реализованного в виде объекта Person.
class PersonFinderStub...
public Person find(long id) {
if (id == 1) {
return new Person("Fowler", "Martin", 10);
}
throw new IllegalArgumentException("Can't find id: " + String.valueOf(id));
}
Я поместил в реестр метод его инициализации в тестовом режиме. Впрочем, поскольку все суррогатное поведение находится в производном классе, я могу отделить весь код, необходимый для выполнения тестирования.
class Registry...
public static void initializeStub() {
soleInstance = new RegistryStub();
}
Пример: реестр, уникальный в пределах потока (Java)
Мэттью Фоммел и Мартин Фаулер
Приведенный выше простой пример не подойдет для многопоточного приложения, в котором у каждого потока должен быть свой реестр. В языке Java существует типовое решение хранилище потока (Thread Specific Storage) [35], реализуемое с помощью локальных переменных потока ThreadLocal. Эти переменные могут быть использованы для создания реестра, уникального в пределах потока.
class ThreadLocalRegistry...
private static ThreadLocal instances = new ThreadLocal();
public static ThreadLocalRegistry getInstance() {
return (ThreadLocalRegistry) instances.get();
}
Для настройки реестра требуются методы, которые будут захватывать и высвобождать его по мере необходимости. Обычно время удержания реестра потоком совпадает с границами транзакции или сеанса.
class ThreadLocalRegistry...
public static void begin() {
Assert.isTrue(instances.get() == null);
instances.set(new ThreadLocalRegistry());
}
public static void end() {
Assert.notNull(getInstance());
instances.set(null);
}
Хранение объекта PersonFinder можно реализовать так же, как и в предыдущем примере.
class ThreadLocalRegistry...
private PersonFinder personFinder = new PersonFinder();;
public static PersonFinder personFinder() {
return getInstance().personFinder;
}
Все обращения к реестру извне будут обрамляться методами begin и end.
try {
ThreadLocalRegistry.begin();
PersonFinder f1 = ThreadLocalRegistry.personFinder();
Person martin = Registry.personFinder().find(1);
assertEquals("Fowler", martin.getLastName());
} finally {ThreadLocalRegistry.end();
}
