Структурные шаблоны: Фасад (Facade)

Название шаблона

Фасад (Facade).

Тип

Структурный шаблон проектирования (Structural).

Описание

FacadeШаблон Фасад объединяет группу объектов в рамках одного специализированного интерфейса и переадресует вызовы его методов к этим объектам.

Шаблон используется если необходимо:

  • упростить доступ к сложной системе;
  • (или) создать различные уровни доступа к системе;
  • (или) уменьшить число зависимостей между системой и клиентом.

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

Назначение Фасада – создать интерфейс, содержащий методы для решения определённой задачи или предоставляющий определённую абстракцию исходной системы. При этом возможно следующее:

  • переадресация вызовов интерфейса шаблона объектам системы;
  • уменьшение числа параметров метода подстановкой заранее определенных значений;
  • создание новых методов, которые объединяют вызовы объектов системы и/или добавляют свою логику;
  • часть исходных методов и свойств будут недоступны через Фасад, т.к. не играют роли для решения поставленной задачи.

Такой подход упрощает использование сложной системы. Одновременно это понижает количество вызываемых методов и общее число их вызовов. В итоге – уменьшение числа зависимостей в приложении.

Важно отметить, что Фасад не требует скрывать используемые системой объекты. Но есть одно важное условие: клиентская часть, для которой создавался интерфейс Фасада, должна использовать только его и не обращаться к объектам системы напрямую. Это способствует более точному отражению задачи клиента в интерфейсе.

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

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

Схожие шаблоны и их отличия

Фасад Объединяет группу объектов под одним специализированным интерфейсом. Упрощает работу с группой объектов, вносит новый уровень абстракции. Содержит или ссылается на объекты, необходимые для реализации специализированного интерфейса.
Адаптер Изменяет интерфейс объекта не изменяя его функциональности. Может адаптировать несколько объектов к одному интерфейсу. Позволяет повторно использовать уже существующий код. Содержит или наследует адаптируемый объект.
Мост Разделяет объект на абстракцию и реализацию. Используется для иерархии объектов. Позволяет отдельно изменять (наследовать) абстракцию и реализацию, повышая гибкость системы. Содержит объект(реализацию), который предоставляет методы для заданной абстракций и ее уточнений (наследников).
Декоратор Расширяет возможности объекта, изменяет его поведение. Поддерживает интерфейс декорируемого объекта, но может добавлять новые методы и свойства. Дает возможность динамически менять функциональность объекта. Является альтернативой наследованию (в том числе множественному). Содержит декорируемый объект. Возможна цепочка объектов, вызываемых последовательно.
Прокси Прозрачно замещает объект и управляет доступом к нему. Не изменяет интерфейс или поведение. Упрощает и оптимизирует работу с объектом. Может добавлять свою функциональность, скрывая ее от клиента. Содержит объект или ссылку на него, может управлять существованием замещенного объекта.
Компоновщик Предоставляет единый интерфейс для взаимодействия с составными объектами и их частями. Упрощает работу клиента, позволяет легко добавлять новые варианты составных объектов и их частей. Включается в виде интерфейса в составные объекты и их части.
Приспособленец

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

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

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

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

Реализация шаблона в общем виде

Возможны следующие подходы к реализации шаблона:

  1. Создание отдельного класса, который ссылается на экземпляр системы. Это классический вариант и реализация отчетливо выделена, т.к. является отдельным объектом.
    public class FacadeImpl : IFacade
    {
        private AppSubSystem _system;
    
        public FacadeImpl(AppSubSubSystem s)
        {
            this._system = s;
        }
    
        /* Skipped */
    }
    Кроме того, экземпляр системы может создаваться внутри Фасада:
    public class FacadeImpl : IFacade
    {
        private readonly AppSubSystem _system = new AppSubSystem();
    
        /* Skipped */
    }
  2. Добавление интерфейса Фасада в систему. Может пригодиться, если у Фасада много клиентов и нет смысла создавать каждому по собственному экземпляру реализации шаблона. 
    public class AppSubSystem 
    {
        public IFacade FacadeInterface  { get; private set; }
    
        public AppSubSystem ()
        {
            this.FacadeInterface = new FacadeImpl(this);
        }
    
        /* Skipped */
    }
  3. Поддержка интерфейса Фасада в классе самой системы, используя наследование. Реализация получается "размытой" по системе. Однако, теперь ее экземпляр может использоваться там, где требуется интерфейс Фасада. Кроме того, появляется возможность доступа к закрытым полям и методам системы, а так же нет необходимости переадресации вызовов одинаковых методов.
    public class AppSubSystem : IFacade { /* Skipped */ }

Все указанные подходы можно записать следующим образом:

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

Пример реализации

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

Начнем с основного – записи в блог, представленной классом BlogPost. Этот класс хранит свойства и содержимое одной записи.

public class BlogPost
{
    /// <summary>Gets or sets the post properties.</summary>
    public BlogPostProperties Properties { get; set; }

    /// <summary>Gets or sets the post content.</summary>
    public BlogPostContent Content { get; set; }
}

Следующий класс BlogPostStorage – хранилище для записей блога на сервере. Он позволяет задавать данные для авторизации, загружать и публиковать записи.

public class BlogPostStorage
{
    /// <summary>Gets or sets the server address.</summary>
    public string Server { get; set; }

    /// <summary>Gets or sets the blogger's login.</summary>
    public string Login { get; set; }

    /// <summary>Gets or sets the blogger's password.</summary>
    public string Password { private get; set; }

    /// <summary>Returns the list that represents the collection of post on server 
    /// for a specified period of time.</summary>
    /// <param name="startDate">The start date.</param>
    /// <param name="endDate">The end date.</param>
    /// <returns>The list that represents the collection of post on server.</returns>
    public List<BlogPostProperties> GetPostList(DateTime startDate, DateTime endDate)
    {
        /* Skipped */
    }

    /// <summary>Gets the post associated with the specified id.</summary>
    /// <param name="postId">The id of the post to get.</param>
    /// <returns>The instance for the id.</</returns>
    public BlogPost GetPost(int postId) { /* Skipped */ }

    /// <summary>Deletes the post associated with the specified id.</summary>
    /// <param name="postId">The id of the post to delete.</param>
    public void DeletePost(int postId) { /* Skipped */ }

    /// <summary>Publishes the specified post.</summary>
    /// <param name="post">The post to publish.</param>
    public void Publish(BlogPost post) { /* Skipped */ }
}

Для проверки орфографии есть простой класс SimpleSpellChecker.

public class SimplepellChecker
{
    /// <summary>Checks the word for spelling errors.</summary>
    /// <param name="word">The word to check.</param>
    /// <param name="suggestions">When this method returns, contains the suggestions 
    /// for the specified word.</param>
    /// <returns>true on correct word; otherwise, false </returns>
    public bool Check(string word, out List<string> suggestions) { /* Skipped */ }
}

И последний из компонентов – текстовый редактор. Уберем практически все его методы для краткости.

public class PostEditor
{
    /// <summary>Spell checker. </summary>
    private readonly SimpleSpellChecker _spellChecker = new SimpleSpellChecker();

    /// <summary>Gets a value indicating whether spell check is complete.</summary>
    public bool IsSpellCheckComplete { get; private set; }

    /// <summary>Gets or sets the post to edit.</summary>
    public BlogPost Post { get; set; }

    /// <summary>Gets the post properties.</summary>
    public BlogPostProperties PostProperties
    {
        get
        {
            if (this.Post == null) { return null; }
            return this.Post.Properties;
        }
    }

    /// <summary>Gets the post content.</summary>
    public BlogPostContent PostContent
    {
        get
        {
            if (this.Post == null) { return null; }
            return this.Post.Content;
        }
    }
}

Перейдем к редактору записей блога. Его подсистема будет выглядеть следующим образом:

public class BlogEditor
{
    private readonly BlogPostStorage _storage = new BlogPostStorage();
    public BlogPostStorage Storage { get { return this._storage; } }

    private readonly PostEditor _editor = new PostEditor();
    public PostEditor Editor { get { return this._editor; } }
}

Клиент может непосредственно использовать эти объекты. Но нужно ли ему это? Если посмотреть внимательно, то выяснится что необходимы следующие функции:

  • указать реквизиты для входа на сервер: Server, Login, Password;
  • загрузить список записей: GetPostList();
  • загрузить запись для редактирования: LoadPost();
  • получить свойства записи и ее текст: PostProperties, PostContent;
  • убедиться что орфография записи проверена: IsSpellCheckComplete;
  • опубликовать запись: Publish();
  • удалить запись: DeletePost().

Все это можно записать в виде интерфейса для Фасада:

public interface IBlogEditorFacade
{
    /// <summary>Gets or sets the server address.</summary>
    string Server { get; set; }

    /// <summary>Sets the blogger's login.</summary>
    string Login { set; }

    /// <summary>Sets the blogger's password.</summary>
    string Password { set; }

    /// <summary>Gets a value indicating whether spell check is complete.</summary>
    bool IsSpellCheckComplete { get; }

    /// <summary>Gets the current post properties.</summary>
    BlogPostProperties PostProperties { get; }

    /// <summary>Gets  the current post content.</summary>
    BlogPostContent PostContent { get; }

    /// <summary>Returns the list that represents the collection of post on server 
    /// for a specified period of time.</summary>
    /// <param name="startDate">The start date.</param>
    /// <param name="endDate">The end date.</param>
    /// <returns>The list that represents the collection of post on server.</returns>
    List<BlogPostProperties> GetPostList(DateTime startDate, DateTime endDate);

    /// <summary>Loads the specified post.</summary>
    /// <param name="postId">The id of the post to get.</param>
    void LoadPost(int postId);

    /// <summary>Publishes the current post to blog.</summary>
    void Publish();

    /// <summary>Deletes the specified post.</summary>
    /// <param name="postId">The id of the post to delete.</param>
    void DeletePost(int postId);
}

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

public class BlogEditorFacade : IBlogEditorFacade
{
    private BlogEditor _blogEditor;

    /// <summary>Initializes a new instance of the
    /// <see cref="BlogEditorFacade"/> class.</summary>
    /// <param name="blogEditor">Reference to a blog editor instance.</param>
    public BlogEditorFacade(BlogEditor blogEditor)
    {
        if (blogEditor == null) {
            throw new ArgumentNullException("blogEditor can't be null");
        }

        this._blogEditor = blogEditor;
    }

    public string Server
    {
        get { return this._blogEditor.Storage.Server; }
        set { this._blogEditor.Storage.Server = value; }
    }

    public void LoadPost(int postId)
    {
        BlogPost post = this._blogEditor.Storage.GetPost(postId);
        if (post != null) {
            this._blogEditor.Editor.Post = post;
        }
    }

  /* IBlogEditorFacade implementation skipped */
}

В конструкторе класса указывается экземпляр системы, к которому Фасад должен обеспечить доступ. Теперь код клиента может использовать интерфейс для упрощения доступа:

public class BlogClient
{
    private readonly IBlogEditorFacade _blogEditor = null;

    public BlogClient(IBlogEditorFacade iBlogEditor)
    {
        this._blogEditor = iBlogEditor;
    }

    public bool ValidateAndPublish()
    {
        bool isPostValid = true;

        BlogPostContent content = this._blogEditor.PostContent;

        /// TODO: Validate the content

        if (isPostValid) {
            this._blogEditor.Publish();
        }

        return isPostValid;
    }
}

Использование BlogClient выглядит следующим образом:

BlogEditor blogEditorObject = new BlogEditor();
/// ...
BlogClient blogClient = new BlogClient(new BlogEditorFacade(blogEditorObject));

Обратите внимание, что в конструктор Фасада передается интерфейс. Кроме того, создание объекта системы (BlogEditor) вынесено из реализации шаблона.

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

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

Возвращаясь к рассматриваемому примеру, можно отметить что, взаимодействие с системой стало проще. Количество методов и вызовов методов ее объектов уменьшилось. Цель применения шаблона достигнута.

Комментарии (10) -

George Koraff 12.11.2010 9:54:19

Приведенный пример не может быть паттерном фасад.
1. Фасад подразумевает что классы системы ничего не знают от фасаде.

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

01  BlogEditor blogEditor = new BlogEditor();
02  IBlogEditorFacade iEditor = (IBlogEditorFacade)blogEditor;

т.е. Вам нужно было фактически остановиться после реализации интерфейса, убрав из конструктора параметр BlogEditor.
в конструкторе : _blogEditor = new BlogEditor();

и пользователь будет пользоваться:
IBlogEditorFacade iEditor = new BlogEditorFacade();
...

C уваженим,
George Koraff

George Koraff :
т.е. Вам нужно было фактически остановиться после реализации интерфейса, убрав из конструктора параметр BlogEditor.
в конструкторе : _blogEditor = new BlogEditor();

и пользователь будет пользоваться:
IBlogEditerFacade iEditor = new BlogEditorFacade();


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

А вот пример пересмотрел. Несколько не корректно составлен (суть была что экземпляр системы все равно где-то создается). Так что за замечание спасибо. Надеюсь поправленный вариант будет более понятным.

Кроме того, если не вмешиваться в класс системы (который содержит подсистемы, в примере это BlogEditor), то как быть с доступом к private методам и поля. Это несколько флеймовая тема, но ведь и такая необходимость может возникнуть.

Мои 5 копеек... Фасад предназначен для сокрытия сложности системы и облегчения работы с ней.
@George Koraff
Клиенты фасада не должны иметь доступа к объектам системы.
Не совсем верно, мы не всегда можем упростить фасад настолько, чтобы работать только с базовыми типами. Повторюсь, фасад только скрывает механизмы взаимодействия объектов в системе, а не сами объекты.
@Andrey
Т.е. вы хотите сказать, что Фасад должен всегда создавать свой экземпляр системы? Такой подход не всегда будет применим, особенно к существующим системам.
Фасад должен получить всё необходимое для создания объекта, но никак не уже созданный объект.

@ osmirnov:
Мои 5 копеек... Фасад предназначен для сокрытия сложности системы и облегчения работы с ней.

Абсолютно согласен.

Фасад должен получить всё необходимое для создания объекта, но никак не уже созданный объект.

А вот тут хотелось бы аргументации такого ограничения.

George Koraff 12.11.2010 13:08:09

@ osmirnov:
Фасад должен предоставлять новый интерфейс, более высокого уровня, скрывая систему, как объекты, так и их взаимодействие.
...чтобы работать только с базовыми типами...
тогда те базовые типы нужно вывести из системы над которой строится фасад, и передовать их как аргументы.

в данном примере типы
BlogPostProperties
BlogPostContent - являются внешними, а сам BlogPost - уже внутренним, и сделовательно клиент будет поьзоваться 3 типами BlogPostProperties, BlogPostContent, BlogEditorFacade

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

@ Andrey:
В табличке про сравнение паттернов - описания даны правильные.
Возьму себе на вооружение, спасибо
P.S. Мне нравятся ваши статьи про паттерны, интересно читать, очень полезно, тем более даются различия между ними, хочу оставить пожелание: чтобы конце когда будет закончен цикл структурных патернов был приведен пример как одно и тоже (систему или класс) можно был бы реализовать различными паттернами.

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

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

George Koraff 12.11.2010 13:59:24

@ osmirnov:
Тут мы уже полезли в дебри. Каждую систему нужно рассматривать индивидуально
Статья про Фасад, дан пример, идет обсуждение примера, никто еще не прокомментировал текст статьи.
Перечитав про этот паттерн в GoF(на русском) могу сделать вывод, что сам текст и описание даны верно, могу только добавить "Фасад не препятствует клиенту напрямую общаться с классами подсистемы, если это необходимо". Исходя из этого в моем первом посте можно полностью вычеркнуть 2 пункт(2. Клиенты фасада не должны иметь доступа к объектам системы.)

@ George Koraff: Спасибо за отзыв. Я приму во внимание ваше предложение, идея хорошая показать на большом примере взаимодействия и варианты. Но обещать не буду.

По поводу общения с подсистемой (и именно с ней) тут есть тонкость. Шаблон не заставляет скрывать объекты подсистемы. Т.е. они так же могут использоваться другими частями приложения. А вот общение с той части приложения, ради которой создан Фасад, ограничивается (ее я и буду считать за клиента). В оригинале сказано следующее

Clients that use the facade don't have to access its subsystem objects directly.

Такое условие направлено на то, чтобы интерфейс фасада более точно соответствовал задаче, решаемой клиентом.

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

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

Вадим 21.01.2012 6:06:30

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

Добавить комментарий