Название шаблона
Фасад (Facade).
Тип
Структурный шаблон проектирования (Structural).
Описание
Шаблон Фасад объединяет группу объектов в рамках одного специализированного интерфейса и переадресует вызовы его методов к этим объектам.
Шаблон используется если необходимо:
- упростить доступ к сложной системе;
- (или) создать различные уровни доступа к системе;
- (или) уменьшить число зависимостей между системой и клиентом.
Прежде всего необходимо утончить, что интерфейс, который предоставляет шаблон, не является суммой всех методов объектов, входящих в систему. Создание такой обобщенной версии приведет к появлению "божественного интерфейса". Т.е. интерфейса с огромным числом методов, без четко выраженной цели и порождающего большое количество зависимостей. В итоге – прямо противоположный шаблону результат.
Назначение Фасада – создать интерфейс, содержащий методы для решения определённой задачи или предоставляющий определённую абстракцию исходной системы. При этом возможно следующее:
- переадресация вызовов интерфейса шаблона объектам системы;
- уменьшение числа параметров метода подстановкой заранее определенных значений;
- создание новых методов, которые объединяют вызовы объектов системы и/или добавляют свою логику;
- часть исходных методов и свойств будут недоступны через Фасад, т.к. не играют роли для решения поставленной задачи.
Такой подход упрощает использование сложной системы. Одновременно это понижает количество вызываемых методов и общее число их вызовов. В итоге – уменьшение числа зависимостей в приложении.
Важно отметить, что Фасад не требует скрывать используемые системой объекты. Но есть одно важное условие: клиентская часть, для которой создавался интерфейс Фасада, должна использовать только его и не обращаться к объектам системы напрямую. Это способствует более точному отражению задачи клиента в интерфейсе.
Кроме того, система может предоставить несколько Фасадов для решения различного рода задач. Это позволяет создать несколько уровней абстракции, который могут быть как одна над другой, так и находиться в одной плоскости. Клиенты могут или использовать определенный Фасад для свой работы, или работать с конкретными объектами напрямую.
Интересен вариант совместного использования шаблонов Фасад и Абстрактной фабрики. В этом случае интерфейс Фасада связан не с самими объектами, а с их интерфейсами. Это скрывает реализацию и дает Абстрактной фабрике возможность изменять ее. Такой подход можно встретить, например, когда система использует платформенно- или аппаратно-зависимые классы.
Схожие шаблоны и их отличия
Фасад | Объединяет группу объектов под одним специализированным интерфейсом. | Упрощает работу с группой объектов, вносит новый уровень абстракции. | Содержит или ссылается на объекты, необходимые для реализации специализированного интерфейса. |
Адаптер | Изменяет интерфейс объекта не изменяя его функциональности. Может адаптировать несколько объектов к одному интерфейсу. | Позволяет повторно использовать уже существующий код. | Содержит или наследует адаптируемый объект. |
Мост | Разделяет объект на абстракцию и реализацию. Используется для иерархии объектов. | Позволяет отдельно изменять (наследовать) абстракцию и реализацию, повышая гибкость системы. | Содержит объект(реализацию), который предоставляет методы для заданной абстракций и ее уточнений (наследников). |
Декоратор | Расширяет возможности объекта, изменяет его поведение. Поддерживает интерфейс декорируемого объекта, но может добавлять новые методы и свойства. | Дает возможность динамически менять функциональность объекта. Является альтернативой наследованию (в том числе множественному). | Содержит декорируемый объект. Возможна цепочка объектов, вызываемых последовательно. |
Прокси | Прозрачно замещает объект и управляет доступом к нему. Не изменяет интерфейс или поведение. | Упрощает и оптимизирует работу с объектом. Может добавлять свою функциональность, скрывая ее от клиента. | Содержит объект или ссылку на него, может управлять существованием замещенного объекта. |
Компоновщик | Предоставляет единый интерфейс для взаимодействия с составными объектами и их частями. | Упрощает работу клиента, позволяет легко добавлять новые варианты составных объектов и их частей. | Включается в виде интерфейса в составные объекты и их части. |
Приспособленец | Не ставит целью изменение интерфейса объекта. Но это может потребоваться для получения обратно данных из вынесенной части состояния. | Позволяет уменьшить число экземпляров объекта в приложении и тем самым сэкономить его ресурсы. | Выносит контекстно-зависимую часть состояния объекта вовне, заменяя несколько его экземпляров одним. |
Отдельно остановимся на двух очень схожих шаблонах – Фасад и Адаптер, использующий несколько адаптируемых объектов. Может появиться вопрос: в чем отличие? Обратите внимание на их цели. Если объединение объектов используется для реализации существующего интерфейса и для повторного использования кода, то это Адаптер. Выделение же сути задачи, упрощение операции, появление нового интерфейса – все это признаки Фасада.
Кстати, в реальных ситуациях шаблоны нужно применять исходя от цели, а не от "сходства". Поэтому и такой вопрос может возникнуть только при изучении.
Реализация шаблона в общем виде
Возможны следующие подходы к реализации шаблона:
- Создание отдельного класса, который ссылается на экземпляр системы. Это классический вариант и реализация отчетливо выделена, т.к. является отдельным объектом.
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 */
}
- Добавление интерфейса Фасада в систему. Может пригодиться, если у Фасада много клиентов и нет смысла создавать каждому по собственному экземпляру реализации шаблона.
public class AppSubSystem
{
public IFacade FacadeInterface { get; private set; }
public AppSubSystem ()
{
this.FacadeInterface = new FacadeImpl(this);
}
/* Skipped */
}
- Поддержка интерфейса Фасада в классе самой системы, используя наследование. Реализация получается "размытой" по системе. Однако, теперь ее экземпляр может использоваться там, где требуется интерфейс Фасада. Кроме того, появляется возможность доступа к закрытым полям и методам системы, а так же нет необходимости переадресации вызовов одинаковых методов.
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 можно передать любого его потомка.
Однако в некоторых случаях выгоднее создавать объект внутри самого шаблона. Например, чтобы каждый экземпляр Фасада имел свой экземпляр системы или чтобы, при необходимости, скрыть используемую систему от клиентов.
Возвращаясь к рассматриваемому примеру, можно отметить что, взаимодействие с системой стало проще. Количество методов и вызовов методов ее объектов уменьшилось. Цель применения шаблона достигнута.