Фасад - паттерн, структурирующий объекты, предоставляя ко всем ним доступ через единый шлюз.
Условия, Задача, Назначение
Предоставляет единый, унифицированный интерфейс ко всей некоторой подсистеме вместо набора отдельных и многочисленных интерфейсов. Фактически, фасад определяет интерфейс более высокого уровня, который упрощает использование подсистемы.
Разбиение на подсистемы облегчает проектирование сложной системы в целом. Общая цель всякого проектирования – свести к минимуму зависимость подсистем друг от друга, а также обмен информацией между ними. Один из способов решения этой задачи – введение общего объекта фасад, предоставляющий единый упрощенный интерфейс к более сложным системным средствам. Можно также сказать, что тем самым мы более приближаемся к построению системы по принципу «черного ящика»: единый интерфейс целой большой системы начинает выступать как интерфейс «черного ящика» и мы не должны знать, как он устроен, какие внутри него взаимосвязи других объектов, сколько их вообще и т.д. Мы просто используем его в соответствии с его интерфейсом, задавая на вход определенные данные и получая соответствующие результаты.
Мотивация
Рассмотрим, например, среду программирования, которая дает приложениям доступ к подсистеме компиляции. В этой подсистеме имеются такие классы, как Scanner (лексический анализатор), Parser (синтаксический анализатор), ProgramNode (узел программы), BytecodeStream (поток байтовых кодов) и ProgramNodeBuilder (строитель узла программы). Все вместе они составляют компилятор. Некоторым специализированным приложениям, возможно, понадобится прямой доступ к этим классам. Но для большинства клиентов компилятора такие детали, как синтаксический разбор и генерация кода, обычно не нужны; им просто требуется откомпилировать некоторую программу. Для таких клиентов применение мощного, но низкоуровневого интерфейса подсистемы компиляции только усложняет задачу.
Чтобы предоставить интерфейс более высокого уровня, изолирующий клиента от этих классов, в подсистему компиляции включен также класс Compiler (компилятор). Он определяет унифицированный интерфейс ко всем возможностям компилятора. Класс Compiler выступает в роли фасада: предлагает простой интерфейс к более сложной подсистеме. Он «склеивает» классы, реализующие функциональность компилятора, но не скрывает их полностью. Благодаря фасаду компилятора работа большинства программистов облегчается: с классом Compiler можно работать просто как с «черным ящиком». Но т.к. в действительности это совсем не закрытый объект, то те, кому нужен доступ к средствам низкого уровня, также не лишаются его.
Признаки применения, использования паттерна Фасад (Facade)
Используйте паттерн фасад, когда:
- Хотите предоставить простой интерфейс к сложной подсистеме.
Часто подсистемы усложняются по мере развития. Применение большинства паттернов приводит к появлению меньших классов, но в большем количестве. Такую подсистему проще повторно использовать и настраивать под конкретные нужды, но вместе с тем применять подсистему без настройки становится труднее. Фасад предлагает некоторый вид системы по умолчанию, устраивающий большинство клиентов. И лишь те объекты, которым нужны более широкие возможности настройки, могут обратиться напрямую к тому, что находится за фасадом;
- Между клиентами и классами реализации абстракции существует много зависимостей.
Фасад позволит изолировать подсистему как от клиентов, так и от других подсистем, что, в свою очередь, способствует повышению степени независимости и переносимости;
- Вы хотите разложить подсистему на отдельные слои: расслоить.
Используйте фасад для определения точки входа на каждый уровень подсистемы. Если подсистемы зависят друг от друга, то зависимость можно упростить, разрешив подсистемам обмениваться информацией только через фасады.
Решение
Участники паттерна Фасад (Facade)
- Facade (Compiler) – фасад.
Осведомлен о том, каким классам подсистемы адресовать запрос.
Делегирует запросы клиентов подходящим объектам внутри подсистемы.
- Классы подсистемы (Scanner, Parser, ProgramNode и т.д.).
Реализуют функциональность подсистемы.
Выполняют работу, запрошенную объектом Facade, которую в свою очередь запросил у Facade-а один из клиентов.
Ничего не «знают» о существовании самого фасада, то есть не хранят ссылок на него.
Схема использования паттерна Фасад (Facade)
Клиенты общаются с подсистемой, посылая запросы фасаду. Он переадресует их подходящим объектам внутри подсистемы. Хотя основную работу выполняют именно объекты подсистемы, фасаду, возможно, придется преобразовать свой интерфейс в интерфейсы подсистемы.
Клиенты, пользующиеся фасадом, не имеют прямого доступа к объектам подсистемы.
Вопросы, касающиеся реализации паттерна Фасад (Facade)
При реализации
фасада следует обратить внимание на следующие вопросы:
- Уменьшение степени связанности клиента с подсистемой.
Степень связанности можно значительно уменьшить, если сделать класс Facade абстрактным. Его конкретные подклассы будут соответствовать различным реализациям подсистемы. Тогда клиенты смогут взаимодействовать с подсистемой через интерфейс абстрактного класса Facade. Это изолирует клиентов от информации о том, какая реализация подсистемы используется.
Вместо порождения подклассов можно просто конфигурировать объект Facade различными объектами подсистем – передавать ему на начальном этапе (создании) соответствующие объекты. Для настройки фасада достаточно будет заменить потом один или несколько таких объектов, группу взаимосвязанных объектов и т.д.
- Открытые и закрытые классы подсистем.
Подсистема похожа на класс в том отношении, что у обоих есть интерфейсы и оба что-то инкапсулируют. Класс - инкапсулирует состояние и операции, а подсистема - классы. И если полезно различать открытый и закрытый интерфейсы класса, то не менее разумно говорить об открытом и закрытом интерфейсах подсистемы.
Открытый интерфейс подсистемы состоит из классов, к которым имеют доступ все клиенты; закрытый же интерфейс доступен только для расширения подсистемы. Класс Facade, конечно же, является частью открытого интерфейса, но это не единственная часть. Другие классы подсистемы также могут быть открытыми. Например, в системе компиляции классы Parser и Scanner - часть открытого интерфейса.
Делать классы подсистемы закрытыми иногда полезно, но это поддерживается немногими объектно-ориентированными языками. Например, и в C++, и в Smalltalk для классов традиционно использовалось глобальное пространство имен. Однако комитет по стандартизации C++ добавил к языку пространства имен, и это позволило разрешать доступ только к открытым классам подсистемы.
- Изолирует клиентов от компонентов подсистемы
Уменьшая тем самым число объектов, с которыми клиентам приходится иметь дело, упрощая работу с подсистемой.
- Позволяет ослабить связанность между подсистемой и ее клиентами.
Зачастую компоненты подсистемы сильно связаны: многие классы из разных частей и слоев системы взаимодействуют с друг другом, меняя один такой класс, приходится перекомпилировать и остальные связанные и т.д. Слабая связанность позволяет видоизменять компоненты, не затрагивая при этом клиентов. Фасады помогают разложить систему на слои и структурировать зависимости между объектами, а также избежать сложных и циклических зависимостей. Это оказывается важным, если клиент и подсистема реализуются независимо.
Уменьшение числа зависимостей на стадии компиляции чрезвычайно важно в больших системах. Хочется, конечно, чтобы время, уходящее на перекомпиляцию после изменения классов подсистемы, было минимальным. Сокращение числа зависимостей за счет фасадов может уменьшить количество нуждающихся в повторной компиляции файлов после небольшой модификации какой-нибудь важной подсистемы. Фасад может также упростить процесс переноса системы на другие платформы, поскольку уменьшается вероятность того, что в результате изменения одной подсистемы понадобится изменять и все остальные.
- Фасад не исключает возможности приложениям напрямую обращаться к классам подсистемы, если это необходимо. Таким образом, у вас есть выбор между простотой и общностью. Совет конечно очевидный: как можно только в крайних случаях прибегайте к прямому взаимодействию с классом за фасадом, лучше если возможно все же как-нибудь обобщить/абстрагировать эту операцию и добавить ее в сам фасад.
Пример
Рассмотрим более подробно, как возвести фасад вокруг подсистемы компиляции.
В подсистеме компиляции определен класс
BytecodeStream, который представляет собой поток объектов
Bytecode. Объект
Bytecode инкапсулирует байтовый код, с помощью которого описываются машинные команды. В этой же подсистеме определен еще класс
Token для объектов, инкапсулирующих лексемы языка программирования.
Класс Scanner принимает на входе поток символов и генерирует поток лексем, по одной каждый раз, т.е. он выступает в роли самого первого уровня обработки исходного кода: Scanner.
Далее класс Parser использует класс ProgramNodeBuilder для построения дерева разбора из лексем, возвращенных классом Scanner.
Parser каждый раз вызывает ProgramNodeBuilder для пошагового построения дерева. Взаимодействие этих классов описывается паттерном строитель.
Дерево разбора состоит из экземпляров подклассов класса ProgramNode, таких как StatementNode, ExpressionNode и т.д. Иерархия классов ProgramNode — это пример паттерна компоновщик. Класс ProgramNode определяет интерфейс для манипулирования узлом программы и его потомками, если таковые имеются.
Операция Traverse (обход) принимает объект CodeGenerator (кодогенератор) в качестве параметра. Подклассы ProgramNode используют этот объект для генерации машинного кода в форме объектов Bytecode, которые помещаются в поток BytecodeStream. Класс CodeGenerator описывается паттерном посетитель.
У
CodeGenerator есть подклассы, например
StackMachineCodeGenerator и
RISCCodeGenerator генерирующие машинный код для различных аппаратных архитектур.
Каждый подкласс ProgramNode реализует операцию Traverse и обращается к ней для обхода своих потомков. Каждый потомок рекурсивно делает то же самое для своих потомков. Например, в подклассе ExpressionNode (узел выражения) операция Traverse определена так: ExpressionNode.
Классы, о которых мы говорили до сих пор, составляют подсистему компиляции. А теперь введем класс Compiler, который будет служить фасадом, позволяющим собрать все эти фрагменты воедино. Класс Compiler предоставляет простой интерфейс для компилирования исходного текста и генерации кода для конкретной машины: Compiler.
В этой реализации жестко «зашит» тип кодогенератора, поэтому программисту не нужно явно задавать целевую архитектуру. Это может быть вполне разумно, когда есть всего одна такая архитектура. Если же это не так, можно было бы изменить конструктор класса
Compiler, чтобы он принимал объект
CodeGenerator в качестве параметра. Тогда программист указывал бы, каким генератором пользоваться при инстанцировании объекта
Compiler. Фасад компилятора можно параметризовать и другими участниками, скажем, объектами
Scanner и
ProgramNodeBuilder, что повышает гибкость, но в то же время сводит на нет основную цель фасада - предоставление упрощенного интерфейса для наиболее распространенного случая.
Известные применения паттерна Фасад (Facade)
Пример компилятора в разделе «Пример кода» навеян идеями из системы компиляции языка
ObjectWorks\Smalltalk.
В каркасе
ЕТ++ приложение может иметь встроенные средства инспектирования объектов во время выполнения. Они реализуются в отдельной подсистеме, включающей класс фасада с именем
ProgrammingEnvironment. Этот фасад определяет такие операции, как
InspectObject и
InspectClass для доступа к инспекторам.
Приложение, написанное в среде
ЕТ++, может также запретить поддержку инспектирования. В таком случае класс
ProgrammingEnvironment реализует соответствующие запросы как пустые операции, не делающие ничего. Только подкласс
ETProgrammingEnvironment реализует эти операции так, что они отображают окна соответствующих инспекторов. Приложению неизвестно, доступно инспектирование или нет. Здесь мы встречаем пример абстрактной связанности между приложением и подсистемой инспектирования.
В операционной системе
Choices фасады используются для составления одного каркаса из нескольких. Ключевыми абстракциями в системе
Choices являются процессы, память и адресные пространства. Для каждой из них есть соответствующая подсистема, реализованная в виде каркаса. Это обеспечивает поддержку переноса
Choices на разные аппаратные платформы. У двух таких подсистем есть «представители», то есть фасады. Они называются
FileSystemlnterface (память) и
Domain (адресные пространства).
Например, для каркаса виртуальной памяти фасадом служит Domain. Класс Domain представляет адресное пространство. Он обеспечивает отображение между виртуальными адресами и смещениями объектов в памяти, файле или на устройстве длительного хранения. Базовые операции класса Domain поддерживают добавление объекта в память по указанному адресу, удаление объекта из памяти и обработку ошибок отсутствия страниц.
Как видно из вышеприведенной диаграммы, внутри подсистемы виртуальной
памяти используются следующие компоненты:
- MemoryObject представляет объекты данных;
- MemoryObjectCache кэширует данные из объектов MemoryObj ects в физической памяти.
MemoryObjectCache - это не что иное, как объект Стратегия, в котором локализована политика кэширования;
- AddressTranslation инкапсулирует особенности оборудования трансляции адресов.
Операция RepairFault вызывается при возникновении ошибки из-за отсутствия страницы. Domain находит объект в памяти по адресу, где произошла ошибка и делегирует операцию RepairFault кэшу, ассоциированному с этим объектом. Поведение объектов Domain можно настроить, заменив их компоненты.
Родственные паттерны
Паттерн
абстрактная фабрика допустимо использовать вместе с фасадом, чтобы предоставить интерфейс для создания объектов подсистем способом, не зависимым от этих подсистем.
Абстрактная фабрика вполне может выступать и как альтернатива фасаду, чтобы скрыть платформенно-зависимые классы.
Паттерн посредник аналогичен фасаду в том смысле, что абстрагирует функциональность существующих классов. Однако назначение посредника - абстрагировать произвольное взаимодействие между «сотрудничающими» объектами.
Часто он централизует функциональность, не присущую ни одному из них. Коллеги посредника обмениваются информацией именно с ним, а не напрямую между собой. Напротив, фасад просто абстрагирует интерфейс объектов подсистемы, чтобы ими было проще пользоваться. Он не определяет новой функциональности, и классам подсистемы ничего неизвестно о его существовании.
Обычно требуется только один фасад. Поэтому объекты фасадов часто бывают
одиночками.