Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Shablony_korporativnykh_prilozheniy_Fauler_M.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
3.82 Mб
Скачать

Реестр (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();

}

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]