Видео курс Шаблоны проектирования

Основные темы, рассматриваемые на уроке:
00:50 1 Метафора
04:09 2 Организация взаимодействия
08:26 3 Рекомендации по применению
10:32 4 Структура паттерна на языке UML
13:07 5 Структура паттерна на языке C#
14:43 6 Участники и отношение между ними
16:23 7 Назначение паттерна
18:14 8 Использование паттерна

Видео урок посвящен шаблону проектирования Facade, который предоставляет унифицированный интерфейс (набор имен методов) вместо интерфейса некоторой подсистемы (набора взаимосвязанных классов или объектов).

Для просмотра полной версии видеокурса и получения доступа к дополнительным учебным материалам Вам необходимо оформить подписку
Оформить подписку

Паттерн Facade

Название

Фасад

Также известен как

Glue (Клей)

Классификация

По цели: структурный

По применимости: к объектам

Частота использования

Высокая                -   1 2 3 4 5

Назначение

Паттерн Facade - предоставляет унифицированный интерфейс (набор имен методов) вместо интерфейса некоторой подсистемы (набора взаимосвязанных классов или объектов).

Класс Facade предоставляет высокоуровневый интерфейс (набор методов одного класса) вместо низкоуровневого интерфейса подсистемы (наборов методов из разных классов входящих в состав подсистемы).

Введение

Большие программные системы достаточно сложно проектировать и сопровождать, поэтому обычно, для облегчения работы, большие системы принято разбивать на подсистемы. Главная задача, решаемая при проектировании – это сокращение количества связей отношений между классами или если сказать по-другому – уменьшение зависимостей классов друг от друга.

Зависимость – это технический термин, который описывает количество имеющихся связей отношений. Если на диаграмме мы видим, что у некоторого класса имеется много связей отношений с другими классами - мы говорим, что такой класс сильно зависит от других классов. А если сказать правильнее – работа экземпляра такого класса будет сильно зависеть от работы экземпляров других классов.

Зависимости классов образуют так называемые привязки. Привязка – это логический термин, который заставляет программиста задуматься над смыслом зависимости. С точки зрения ООП, привязки бывают двух типов: хорошие - бизнес привязки и плохие - технические привязки. Бизнес привязки выражают требования бизнеса, например, класс Customer связан связью отношения ассоциации с классом Order. Технические привязки выражают системные требования, например, класс Customer связан связью отношения ассоциации с классом DataSet. Важно заметить, что без технических привязок не обойтись, и техническая привязка может стать условно-хорошей тогда и только тогда, когда зависимые сущности находятся в разных слоях системы (например, класс Customer располагается в Business Layer, а DataSet располагается в слое Data Layer), в таком случае при анализе бизнес логики программной системы можно пренебречь связями, ведущими в нижележащий слой.

Имеются привязки, которые не попадают под классификацию хороших и плохих привязок – это «вздорные» привязки, например, класс Order связан связью отношения ассоциации с классом Customer, или класс Customer связан связью отношения агрегации с классом Order. Понятно, что связь отношения ассоциации символизирует знание о чем-то или о ком-то и звучит «знаю о или использую это», а связь отношения агрегации символизирует составление из частей и звучит «состою из или включаю в себя». Поэтому приведенные выше примеры «вздорных» связей будут звучать так: Order знает о Customer (заказ знает о человеке), или Customer состоит из Order (человек состоит из заказа). Связь Order знает о Customer, можно заменить, например, на Order состоит из CustomersDetails. Так же класс Order может использовать при построении заказа, например, калькулятор расчета скидок DiscountCalculator, такая связь выражается ассоциацией, а не агрегацией, так как не очень удачным решением будет создавать странную гибридную сущность - «заказо-скидко-калькулятор». 

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

Возможно ли выразить количественно силу зависимости? Конечно, сила зависимости выражается при помощи такого понятия как связанность (coupling).  Связанность – есть мера зависимости. Связанность имеет более десятка метрик, позволяющих получить численное значение силы зависимости. Детальное рассмотрение метрик связанности не входит в контекст данной книги.

Одним из способов сведения зависимостей к минимуму, это использование паттерна Facade.

На диаграмме слева, видно, что из клиентского кода происходит много обращений к классам подсистем. На диаграмме справа, вводится дополнительный ассоциативный класс Facade, через единый и упрощенный интерфейс которого происходит взаимодействие с классами подсистем. 

Структура паттерна на языке UML

См. пример к главе: \010_Facade\001_Facade

Структура паттерна на языке C#

См. пример к главе: \010_Facade\001_Facade

Участники

  • Facade - Фасад:

Перенаправляет запросы клиентов, другим объектам из которых состоит подсистема.

  • Classes subsystemsКлассы подсистем:

Реализуют фактическую функциональность системы. Ничего не знают о фасаде (не имеют ссылок на фасад).

Отношения между участниками

Отношения между классами

  • Класс Facade связан связями отношения ассоциации с классами подсистем (Subsystem).

Отношения между объектами

  • Клиенты общаются с объектами подсистем, через фасад. Фасад переадресует запросы клиентов подходящим объектам подсистемы.
  • Клиенты, которые используют фасад, не должны параллельно работать с объектами подсистем напрямую.

Мотивация

В качестве примера использования паттерна Facade, рассмотрим простейшую программу-компилятор, для ограниченного числа мнемоник (инструкций) языка Assembler

Имеется простейшая программа на языке Assembler, которую требуется преобразовать в исполняемый файл result.exe:

 

var1 dd 5         ; Переменной с именем var1, типа dd, присваивается значение 5.

var2 dd 2

 

mov eax, var1 ; В регистр процессора – eax, помещается значение переменной var1.

add eax, var2  ; К значению в регистре, прибавляется значение переменной var2.

 

call HexMessage

call ExitProcess

 

В компиляторе имеются следующие классы: Scanner (лексический анализатор), Parser (синтаксический анализатор), ProgramNode (узел программы), BytecodeStream (поток байтовых кодов), ProgramNodeBuilder (строитель узла программы) и другие.

Для того чтобы произвести компиляцию программы, клиенту требуется понимать работу всех классов, составляющих систему компиляции. Чтобы упростить способ взаимодействия с системой компиляции, есть смысл включить в систему класс Compiler (компилятор). Класс Compiler предоставляет высокоуровневый интерфейс для упрощенного взаимодействия с подсистемой компиляции и будет играть роль фасада этой подсистемы. Теперь, для того чтобы произвести компиляцию, потребуется объекту-компилятору, передать исходный код программы, указать имя исполняемого файла и передать ссылку на механизм обработки сообщений от компилятора.

class Program
{
    public static void Main()
    {
        string sourceCode = "var1 dd 5; var2 dd 2;" +
                            "mov eax, var1;" +
                            "add eax, var2;" +
                            "call HexMessage;" +
                            "call ExitProcess;";

        Action<string> message = msg => Console.WriteLine(msg);
        Compiler compiler = new Compiler();
        compiler.Compile(sourceCode, " result.exe", message);        
    }
}

Благодаря использованию объекта фасада, работа программиста облегчается, так как не нужно вникать в логику работы подсистемы компиляции, при этом, остается возможность обращения к классам подсистем, когда это может потребоваться. Например, чтобы получить коллекцию токенов (Token) каждый из которых предоставляет набор лексем, можно обратиться к сканеру (Scanner) напрямую.

Благодаря использованию объекта фасада, работа программиста облегчается, так как не нужно вникать в логику работы подсистемы компиляции, при этом, остается возможность обращения к классам подсистем, когда это может потребоваться. Например, чтобы получить коллекцию токенов (Token) каждый из которых предоставляет набор лексем, можно обратиться к сканеру (Scanner) напрямую.

class Program
{
    public static void Main()
    {
        string sourceCode = "var1 dd 5; var2 dd 2;" +
                            "mov eax, var1;" +
                            "add eax, var2;" +
                            "call HexMessage;" +
                            "call ExitProcess;";

        Scanner scanner = new Scanner();
        List<Token> tokens = scanner.Scan(sourceCode);
    }
}

См. пример к главе: \010_Facade\002_Assembler Compiler

Применимость паттерна

Паттерн Facade рекомендуется использовать, когда:

  • Требуется предоставить высокоуровневый интерфейс для замены множества низкоуровневых интерфейсов.
  • Между клиентскими классами и классами подсистем имеется много зависимостей.
  • Требуется разложить систему на слои, при этом объект-фасад будет использоваться в качестве точки входа в слой.

Результаты

Паттерн Facade обладает следующими преимуществами:

  • Сокрытие устройства подсистемы.

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

  • Ослабление связанности между клиентским кодом и объектами подсистем.

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

Например, в жизни людям проще пользоваться современным автомобилем, чем автомобилем начала 19 века. Старинный автомобиль требует от водителя знания множества тонкостей обращения, начиная от запуска двигателя (проворачивают коленвал пусковой (заводной) рукояткой - «кривой стартёр») и заканчивая особенностями управления автомобилем в процессе его движения. Современный автомобиль заводится просто, различные системы контроля делают процесс движения автомобиля безопасным как в дождь, гололедицу, в большом потоке транспорта, позволяют безопасно парковать автомобиль и предоставляют много других полезных особенностей. Можно сказать, что интерфейс взаимодействия с современным автомобилем проще интерфейса чем со старинным автомобилем, при этом интерфейс взаимодействия с новым автомобилем функционально богаче интерфейса взаимодействия со старым автомобилем. Водитель взаимодействует с автомобилем через «фасад» представляющий собой руль, педали, ручку переключения скоростей и множество различных кнопок. Фасад автомобиля скрывает за собой части, из которых состоит, например, двигатель автомобиля.

  • Использование объектов подсистем напрямую.

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

Реализация

Полезные приемы реализации паттерна Facade:

  • Уменьшение связанности клиента с подсистемой.

Связанность можно уменьшить, если класс Facade будет абстрактным, а производные от него, конкретные классы ConcreteFacadeA и ConcreteFacadeB будут соответствовать различным реализациям подсистем. Например, требуется создать компилятор генерирующий машинные коды для различных аппаратных архитектур, таких как «Intel х86 family» и «Motorola 68000 family». Класс Compiler будет абстрактным, а от него будут наследоваться два конкретных класса CompilerIntelx86 и CompilerMotorola68000. В таком случае клиенты смогут взаимодействовать с подсистемой компиляции через унифицированный интерфейс абстрактного класса Compiler (не замечая различия между особенностями архитектуры Фон-Неймана (Intel) и особенностями Гарвардской архитектуры (Motorola)).

  • Открытые и закрытые классы подсистем.

Иногда полезно делать некоторые классы подсистемы закрытыми от клиента (имеется ввиду внутренними - internal), размещая их в отдельной сборке и помечая модификатором доступа internal. Класс Facade должен принадлежать к открытой части сборки и должен быть помеченным модификатором доступа - public.

См. пример к главе: \010_Facade\003_OpenAndCloseClasses

В примере с компилятором к открытой части подсистемы компиляции можно отнести класс Compiler, а также важные классы которые в отдельных случаях есть смысл использовать самостоятельно: Scanner и Parser, все остальные классы есть смысл отнести к закрытой части.

Пример кода

Предлагается более подробно рассмотреть пример из раздела «Мотивация»: «Реализация подсистемы компиляции исходного кода на языке Assembler в машинный код».

Класс Compiler представляет собой фасад, для организации удобного и безопасного доступа к компонентам подсистемы компиляции.

public class Compiler
{
    Scanner scanner;
    Parser parser;
    ProgramNodeBuilder builder;
    CodeGenerator generator;
    ByteCodeStream stream;

    public Compiler()
    {
        this.stream = new ByteCodeStream();
        this.scanner = new Scanner();
        this.parser = new Parser();
        this.generator = new CISCCodeGenerator();
        this.builder = new ProgramNodeBuilder(this.generator);
    }

    public void Compile(string sourceCode, string exeFileLocation, 
                                           Action<string> notification)
    {
         try
         {
             parser.Parse(scanner, builder, sourceCode);

             ProgramNode product = builder.GetProgramNode();
             this.stream.SaveStreamToFile(product.ProgramByteCode, exeFileLocation);
             notification("Компиляция завершилась успешно.");
         }
         catch (Exception ex)
         {
             string errorMessage = 
                                 string.Format("Компиляция не удалась.\r\nОШИБКА: ");
             errorMessage += ex.GetType().Name + ":\n" + ex.Message;
             notification(errorMessage);
         }
     }
}

Класс Scanner разбивает программный код на отдельные инструкции представленные токенами (Token).

public class Scanner
{
    List<Token> tokens = new List<Token>();

    public List<Token> Scan(string sourceCode)        
    {
        string[] commands = sourceCode.Trim().Split(Constants.programDelimiter,             
                                           StringSplitOptions.RemoveEmptyEntries);
        foreach (string command in commands)
        {
            string[] lexemes = command.Split(Constants.lexemesDelimiter,  
                                             StringSplitOptions.RemoveEmptyEntries);
            this.tokens.Add(new Token(lexemes));
        }

        return tokens;
    }
}

Класс Parser проверяет правильность (допустимость) инструкций, анализируя лексемы из токенов. И дает объекту класса ProgramNodeBuilder, команды для построения узлов дерева в соответствии с найденным токеном.

internal class Parser
{
    Dictionary<string, int> variables = new Dictionary<string, int>();
    Scanner scanner;
    ProgramNodeBuilder builder;

    public void Parse(Scanner scanner, ProgramNodeBuilder builder, string sourceCode)
    {
        this.scanner = scanner;
        this.builder = builder;

        // Разбить программу на токены состоящие из лексем.
        List<Token> tokens = scanner.Scan(sourceCode);
        // Проверить каждый токен состоящий из лексем на ошибки.
        foreach (Token token in tokens)
            ParseToken(token.Lexemes);

        builder.Build(tokens);        
    }

    private void ParseToken(List<string> lexemes)
    {
        // Полная реализация данного метода в примере к главе.
    }

    private void VerifyParameters(string[] parameters, Command command)
    {
        // Полная реализация данного метода в примере к главе.
    }

    private void VerificationVariableName(string name)
    {
        // Полная реализация данного метода в примере к главе.
    }
}

Класс ProgramNodeBuilder используется для построения дерева разбора, состоящего из экземпляров подклассов класса ProgramNode.

internal class ProgramNodeBuilder
{
    CodeGenerator codeGenerator = null;
    ProgramNode programNode = new ExpressionNode();

    public ProgramNodeBuilder(CodeGenerator codeGenerator)
    {
        this.codeGenerator = codeGenerator;
    }

    public void Build(List<Token> tokens)
    {
        codeGenerator.Initialize();
        programNode.Traverse(codeGenerator);
        programNode.ProgramByteCode = codeGenerator.GenerateByteCode();
    }

    public ProgramNode AddVariableNode(string name, int value)
    {
        VariableNode node = new VariableNode(name, value);
        programNode.AddNode(node);
        return node;
    }

    public ProgramNode AddStatementNode(string name, string[] parameters)
    {
        StatementNode node = new StatementNode(name, parameters);
        programNode.AddNode(node);
        return node;
    }

    public ProgramNode GetProgramNode()
    {
        return this.programNode;
    }
}

Абстрактный класс ProgramNode является базовым классом для классов ExpressionNode, StatementNode, и VariableNode, экземпляры которых будут представлены элементами в дереве разбора.

abstract class ProgramNode
{
    public ByteCode ProgramByteCode { get; set; }
    public abstract void Traverse(CodeGenerator generator);
    public abstract void AddNode(ProgramNode node);
}

Класс ExpressionNode представляет собой узловой элемент дерева разбора.

class ExpressionNode : ProgramNode
{
    List<ProgramNode> nodes = new List<ProgramNode>();

    public override void AddNode(ProgramNode node)
    {
        nodes.Add(node);
    }

    public override void Traverse(CodeGenerator generator)
    {
        foreach (var item in nodes)
            item.Traverse(generator);
    }
}

Класс StatementNode представляет собой листовой элемент, определяющий команды в исходном коде.

class StatementNode : ProgramNode
{
    public StatementNode()
    {
        Offsets = new List<int>();
    }

    public StatementNode(string name, string[] parameters)
    {
        Name = name;
        Parameters = parameters;
        Offsets = new List<int>();
    }

    public string Name { get; set; }
    public List<int> Offsets { get; set; }
    public int Address { get; set; }
    public string[] Parameters { get; set; }

    public override void AddNode(ProgramNode node)
    {
        throw new InvalidOperationException();
    }

    public override void Traverse(CodeGenerator generator)
    {
        // Полная реализация данного метода в примере к главе.
    }
}

Класс VariableNode представляет собой листовой элемент определяющий переменную в исходном коде.

class VariableNode : ProgramNode
{
    public string Name { get; set; }
    public int Value { get; set; }
    public byte[] Address { get; set; }

    public VariableNode(string name, int value)
    {
        Name = name; 
        Value = value;
    }

    public override void AddNode(ProgramNode node)
    {
        throw new InvalidOperationException();
    }

    public override void Traverse(CodeGenerator generator)
    {
        generator.SetDataVariable(this);
    }
}

Класс Token хранит в себе набор лексем (частей, составляющих инструкцию ассемблера).

public class Token
{
    public List<string> Lexemes { get; set;}

    public Token(){  }

    public Token(string [] lexemes)
    {
        this.Lexemes = new List<string>();
        this.Lexemes.AddRange(lexemes);
    }
}

Абстрактный класс CodeGenerator является базовым для класса CISCCodeGenerator и возможных классов-генераторов машинных кодов для других аппаратных архитектур.

abstract class CodeGenerator
{
    // Полная реализация данного класса в примере к главе.

    public abstract void Initialize();
    public abstract ByteCode GetNopCode();
    public abstract ByteCode GetCallCode(string[] parameters);
    public abstract ByteCode GetXorCode(string[] parameters);
    public abstract ByteCode GetSubCode(string[] parameters);
    public abstract ByteCode GetAddCode(string[] parameters);
    public abstract ByteCode GetMovCode(string[] parameters);
    public abstract void SetDataVariable(VariableNode node);
    public abstract void FixFunctionAddresses();
    public abstract ByteCode GenerateByteCode();
}

Класс CISCCodeGenerator генерирует машинные коды для аппаратной архитектуры «Intel х86 family».

class CISCCodeGenerator : CodeGenerator
{
    // Полная реализация данного класса в примере к главе.
}

Класс ByteCode представляет собой объектно-ориентированное представление машинного кода, полученного из узлов дерева разбора.

class ByteCode
{
    byte[] code = null;

    public byte[] Code
    {
        get { return code; }
    }

    public ByteCode(params byte[] code)
    {
       this.code = code;
    }
}

Класс ByteCodeStream выполняет сохранение байт-кода всей программы в файл.

class ByteCodeStream
{
    public void SaveStreamToFile(ByteCode programByteCode, string exeFileLocation)
    {
        using (FileStream fileStream = File.Create(exeFileLocation, 
                                                   programByteCode.Code.Length))
        {
            fileStream.Write(programByteCode.Code, 0, programByteCode.Code.Length);
        }
    }
}

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

© 2017 ITVDN, все права защищены