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

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

Фасад (Facade).

Тип

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

Описание

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. Создание отдельного класса, который ссылается на экземпляр системы. Это классический вариант и реализация отчетливо выделена, т.к. является отдельным объектом.

public class FacadeImpl : IFacade { private AppSubSystem _system;

public FacadeImpl(AppSubSubSystem s)
{
    this._system = s;
}

/* Skipped */

}


Кроме того, экземпляр системы может создаваться внутри Фасада: 

    ```csharp
public class FacadeImpl : IFacade
{
    private readonly AppSubSystem _system = new AppSubSystem();

    /* Skipped */
}
  1. Добавление интерфейса Фасада в систему. Может пригодиться, если у Фасада много клиентов и нет смысла создавать каждому по собственному экземпляру реализации шаблона.

public class AppSubSystem { public IFacade FacadeInterface { get; private set; }

public AppSubSystem ()
{
    this.FacadeInterface = new FacadeImpl(this);
}

/* Skipped */

}

3. Поддержка интерфейса Фасада в классе самой системы, используя наследование. Реализация получается "размытой" по системе. Однако, теперь ее экземпляр может использоваться там, где требуется интерфейс Фасада. Кроме того, появляется возможность доступа к закрытым полям и методам системы, а так же нет необходимости переадресации вызовов одинаковых методов. 
    
    ```csharp
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 можно передать любого его потомка.

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

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