Русский
Русский
English
Статистика
Реклама

Design pattern

Из песочницы Enum и switch, и что с ними не так

04.09.2020 16:11:02 | Автор: admin

image


Часто ли у вас было такое, что вы добавляли новое значение в enum и потом тратили часы на то, чтобы найти все места его использования, а затем добавить новый case, чтобы не получить ArgumentOutOfRangeException во время исполнения?


Идея


Если проблема состоит только в switch операторе и отслеживании новых типов, тогда давайте избавимся от них!


Идея состоит в том, чтобы заменить использование switch паттерном visitor.


Пример 1


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


Определим файл DocumentType.cs:


public enum DocumentType{    Invoice,    PrepaymentAccount}public interface IDocumentVisitor<out T>{    T VisitInvoice();    T VisitPrepaymentAccount();}public static class DocumentTypeExt{    public static T Accept<T>(this DocumentType self, IDocumentVisitor<T> visitor)    {        switch (self)        {            case DocumentType.Invoice:                return visitor.VisitInvoice();            case DocumentType.PrepaymentAccount:                return visitor.VisitPrepaymentAccount();            default:                throw new ArgumentOutOfRangeException(nameof(self), self, null);        }    }}

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


Опишем visitor который будет искать в базе нужный документ DatabaseSearchVisitor.cs:


public class DatabaseSearchVisitor : IDocumentVisitor<IDocument>{    private ApiId _id;    private Database _db;    public DatabaseSearchVisitor(ApiId id, Database db)    {        _id = id;        _db = db;    }    public IDocument VisitInvoice() => _db.SearchInvoice(_id);    public IDocument VisitPrepaymentAccount() => _db.SearchPrepaymentAccount(_id);}

И потом его использование:


public void UpdateStatus(ApiDoc doc){    var searchVisitor = new DatabaseSearchVisitor(doc.Id, _db);    var databaseDocument = doc.Type.Accept(searchVisitor);    databaseDocument.Status = doc.Status;    _db.SaveChanges();}

Пример 2


У нас есть события, которые выглядят следующим образом:


public enum PurseEventType{    Increase,    Decrease,    Block,    Unlock}public sealed class PurseEvent{    public PurseEventType Type { get; }    public string Json { get; }    public PurseEvent(PurseEventType type, string json)    {        Type = type;        Json = json;    }}

Мы хотим отправлять уведомления пользователю на определенный тип событий. Тогда реализуем visitor:


public interface IPurseEventTypeVisitor<out T>{    T VisitIncrease();    T VisitDecrease();    T VisitBlock();    T VisitUnlock();}public sealed class PurseEventTypeNotificationVisitor : IPurseEventTypeVisitor<Missing>{    private readonly INotificationManager _notificationManager;    private readonly PurseEventParser _eventParser;    private readonly PurseEvent _event;    public PurseEventTypeNotificationVisitor(PurseEvent @event, PurseEventParser eventParser, INotificationManager notificationManager)    {        _notificationManager = notificationManager;        _event = @event;        _eventParser = eventParser;    }    public Missing VisitIncrease() => Missing.Value;    public Missing VisitDecrease() => Missing.Value;    public Missing VisitBlock()    {        var blockEvent = _eventParser.ParseBlock(_event);        _notificationManager.NotifyBlockPurseEvent(blockEvent);        return Missing.Value;    }    public Missing VisitUnlock()    {        var blockEvent = _eventParser.ParseUnlock(_event);        _notificationManager.NotifyUnlockPurseEvent(blockEvent);        return Missing.Value;    }}

Для примера не будем ничего возвращать. Для этого можно воспользоваться типом Missing из System.Reflection или же написать тип Unit. В реальном проекте возвращался бы Result, например, с информацией об ошибке, если такие имеются.


И пример использования:


public void SendNotification(PurseEvent @event){    var notificationVisitor = new PurseEventTypeNotificationVisitor(@event, _eventParser, _notificationManager);    @event.Type.Accept(notificationVisitor);}

Дополнение


Если нужно быстрее


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


Метод расширение:


public static T Accept<TVisitor, T>(this DocumentType self, in TVisitor visitor)    where TVisitor : IDocumentVisitor<T>    {        switch (self)        {            case DocumentType.Invoice:                return visitor.VisitInvoice();            case DocumentType.PrepaymentAccount:                return visitor.VisitPrepaymentAccount();            default:                throw new ArgumentOutOfRangeException(nameof(self), self, null);        }    }

Сам visitor остаётся прежним, только меняем class на struct.


И сам код обновления документа выглядит не так удобно, но работает быстро:


public void UpdateStatus(ApiDoc doc){    var searchVisitor = new DatabaseSearchVisitor(doc.Id, _db);    var databaseDocument = doc.Type.Accept<DatabaseSearchVisitor, IDocument>(searchVisitor);    databaseDocument.Status = doc.Status;    _db.SaveChanges();}

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


Читабельность и in-place реализация


Если нужно реализовать логику только в одном месте, то часто visitor громоздко и не удобно. Поэтому есть альтернативное решение match.


Сразу пример со структурой:


public static T Match<T>(this DocumentType self, Func<T> invoiceCase, Func<T> prepaymentAccountCase){    var visitor = new FuncVisitor<T>(invoiceCase, prepaymentCase);    return self.Accept<FuncVisitor<T>, T>(visitor);}

Сам FuncVisitor:


public readonly struct FuncVisitor<T> : IDocumentVisitor<T>{    private readonly Func<T> _invoiceCase;    private readonly Func<T> _prepaymentAccountCase;    public FuncVisitor(Func<T> invoiceCase, Func<T> prepaymentAccountCase)    {        _invoiceCase = invoiceCase;        _prepaymentAccountCase = prepaymentAccountCase;    }    public T VisitInvoice() => _invoiceCase();    public T VisitPrepaymentAccount() => _prepaymentAccountCase();}

Использование match:


public void UpdateStatus(ApiDoc doc){    var databaseDocument = doc.Type.Match(        () => _db.SearchInvoice(doc.Id),        () => _db.SearchPrepaymentAccount(doc.Id)    );    databaseDocument.Status = doc.Status;    _db.SaveChanges();}

Итог


При добавлении нового значения в enum необходимо:


  1. Добавить метод в интерфейс.
  2. Добавить его использование в метод расширение.

Для остальных мест компилятор подскажет нам, где необходимо реализовать новый метод.
Таким образом мы избавляемся от проблемы забытого case в switch.


Это все еще не серебряная пуля, но может здорово помочь в работе с enum.


Ссылки


Подробнее..

Перевод Реализация паттерна проектирования

10.12.2020 14:11:17 | Автор: admin

Будущих студентов курса "Unity Game Developer. Professional" приглашаем посмотреть открытый урок на тему "Продвинутый искусственный интеллект врагов в шутерах".


А сейчас делимся традиционным переводом полезного материала.


В этом туториале мы освоим паттерн проектирования Команда (Command) и реализуем его в Unity в рамках системы перемещения игрового объекта.

Знакомство с паттерном Команда

Запросы, приказы и команды: все мы знакомы с ними в реальной жизни; один человек отправляет запрос (или приказ, или команду) другому человеку выполнить (или не выполнять) некоторые задачи, которые ему поручены. В проектировании и разработке программного обеспечения это работает аналогичным образом: запрос одного компонента передается другому для выполнения определенных задач в рамках паттерна Команда.

Определение: Паттерн Команда это поведенческий паттерн проектирования, в котором запрос преобразуется в объект, который инкапсулирует (содержит) всю информацию, необходимую для выполнения действия или запуска события в более позднее время. Это преобразование в объекты позволяет параметризовать методы различными запросами, задерживать выполнение запроса и/или ставить его в очередь.

Хороший и надежный программный продукт должен основываться на принципе разделения обязанностей. Обычно его можно воплотить, разбив приложение на несколько уровней (или программных компонентов). Часто встречающийся на практике пример разделение приложения на два уровня: графический интерфейс пользователя (GUI), который отвечает только за графическую часть, и логический обработчик (logic handler), который реализует бизнес-логику.

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

Структура паттерна Команда

Ниже в виде UML диаграммы классов представлена структура паттерна Команда. Классы, входящие в диаграмму, подробно описаны ниже.

Диаграмма классов для паттерна проектирования Команда

Для реализации паттерна Команд нам потребуются абстрактный класс Command, конкретные команды (ConcreteCommandN) и классы Invoker, Client и Receiver.

Command

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

public interface ICommand      {     void Execute();    void ExecuteUndo();       }

Invoker

Класс Invoker (также известный как Sender) отвечает за инициирование запросов. Это класс, запускающий необходимую команду. Этот класс должен иметь переменную, в которой хранится ссылка на объект команды или его контейнер. Инвокер вместо непосредственной отправки запроса получателю запускает команду. Обратите внимание, что инвокер не несет ответственности за создание объекта команды. Обычно он получает заранее созданную команду от клиента через конструктор.

Client

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

Receiver (опциональный класс)

Класс Receiver (получатель) это класс, который принимает команду и содержит в основном всю бизнес-логику. Практически любой объект может выступать в качестве получателя. Большинство команд обрабатывают только детали того, как запрос передается получателю, в то время как сам получатель выполняет фактическую работу.

Конкретные команды

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

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

Реализация паттерна Команда в Unity

Как уже говорилось выше, мы собираемся реализовать паттерн Команда в Unity для решения задачи перемещения игрового объекта путем применения различных типов перемещения. Каждый из этих типов перемещения будет реализован как команда. Мы также реализуем функцию отмены (Undo), чтобы иметь возможность отменять операции в обратном порядке.

Итак, начнем!

Создание нового 3D проекта Unity

Мы начнем с создания 3D проекта Unity. Назовем его CommandDesignPattern.

Создание поверхности

Для этого урока мы создадим простой объект Plane, который будет формировать нашу поверхность для перемещения. Кликните правой кнопкой мыши окно Hierarchy и создайте новый игровой объект Plane. Переименуйте его в Ground и измените размер до 20 единиц по оси X и 20 единиц по оси z. Вы можете применить цвет или наложить текстуру на поверхность по своему вкусу, чтобы она выглядела более привлекательно.

Создание игрока

Теперь мы создадим игровой объект Player. В этом туториале для представления игрока мы будем использовать объект Capsule. Кликните правой кнопкой мыши в окно Hierarchy и создайте новый игровой объект Capsule. Переименуйте его в Player.

Создание скрипта GameManager.cs

Выберите игровой объект Ground и добавьте новый скриптовый компонент. Назовите скрипт GameManager.cs.

Теперь мы реализуем перемещение объекта Player.

Для этого мы добавляем public GameObject переменную с именем player.

public GameObject mPlayer;

Теперь перетащите игровой объект Player из Hierarchy в поле Player в окне инспектора.

Реализация движений игрока

Для перемещения игрока мы будем использовать клавиши со стрелками (Up, Down, Left и Right).

Для начала реализуем движение самым простым способом. Реализовать его мы будем в методе Update. Для простоты реализуем дискретное перемещение на 1 единицу на каждое нажатие клавиши в соответствующих направлениях.

void Update(){    Vector3 dir = Vector3.zero;    if (Input.GetKeyDown(KeyCode.UpArrow))        dir.z = 1.0f;    else if (Input.GetKeyDown(KeyCode.DownArrow))        dir.z = -1.0f;    else if (Input.GetKeyDown(KeyCode.LeftArrow))        dir.x = -1.0f;    else if (Input.GetKeyDown(KeyCode.RightArrow))        dir.x = 1.0f;    if (dir != Vector3.zero)    {        _player.transform.position += dir;    }}

Нажмите кнопку Play и посмотрите, что получилось. Нажимайте клавиши со стрелками (Up, Down, Left и Right), чтобы увидеть движение игрока.

Реализация движения по клику

Теперь мы реализуем перемещение по клику правой кнопкой мыши Player должен будет переместиться в место на Ground, по которому был произведен клик. Как же мы это сделаем?

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

public Vector3? GetClickPosition(){    if(Input.GetMouseButtonDown(1))    {        RaycastHit hitInfo;        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);        if(Physics.Raycast(ray, out hitInfo))        {            //Debug.Log("Tag = " + hitInfo.collider.gameObject.tag);            return hitInfo.point;        }    }    return null;}

Что это за возвращаемый тип Vector3?

Использование оператора ? для возвращаемых типов в C#, например

public int? myProperty { get; set; }

означает, что тип значения со знаком вопроса является nullable типом

Nullable типы, являются экземплярами структуры System.Nullable. Тип, допускающий значение NULL, может представлять корректный диапазон значений для своего базового типа значения плюс дополнительное значение NULL. Например, Nullable&lt;Int32>, который произносится как Nullable of Int32, может быть присвоено любое значение от -2147483648 до 2147483647, а также ему может быть присвоен null. Nullable&lt;bool> может быть присвоено значение true, false или null. Возможность назначать null числовым и логическим типам особенно полезна, когда вы имеете дело с базами данных и другими типами, которые содержат элементы, которым может не быть присвоено значение. Например, логическое поле в базе данных может хранить значения true или false, либо оно может быть еще не определено.

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

public IEnumerator MoveToInSeconds(GameObject objectToMove, Vector3 end, float seconds){    float elapsedTime = 0;    Vector3 startingPos = objectToMove.transform.position;    end.y = startingPos.y;    while (elapsedTime < seconds)    {        objectToMove.transform.position = Vector3.Lerp(startingPos, end, (elapsedTime / seconds));        elapsedTime += Time.deltaTime;        yield return null;    }    objectToMove.transform.position = end;}

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

Изменим метод Update, добавив следующие строки кода.

****    var clickPoint = GetClickPosition();    if (clickPoint != null)    {        IEnumerator moveto = MoveToInSeconds(_player, clickPoint.Value, 0.5f);        StartCoroutine(moveto);    }****

Нажмите кнопку Play и посмотрите, что получилось. Нажимайте клавиши со стрелками (Up, Down, Left и Right) и кликайте правой кнопкой мыши по Ground, чтобы увидеть перемещение объекта Player.

Реализация операции отмены

Как реализовать операцию отмены (Undo)? Где нужна отмена движения? Попробуйте догадаться сами.

Реализация паттерна Команда в Unity

Мы собираемся реализовать метод Undo для каждой операции перемещения, которую мы можем выполнять как с помощью нажатия клавиш, так и кликом правой кнопки мыши.

Самый простой способ реализовать операцию Undo использовать паттерн проектирования Команда, реализовав его в Unity.

В рамках этого паттерна мы преобразуем все типы движения в команды. Начнем с создания интерфейса Command.

Интерфейс Command

public interface ICommand{    void Execute();    void ExecuteUndo();}

Наш интерфейс Command имеет два метода. Первый это обычный метод Execute, а второй метод ExecuteUndo, выполняющий операцию отмены. Для каждой конкретной команды нам нужно будет реализовать эти два метода (помимо других методов, если они будут необходимы).

Теперь давайте преобразуем наше базовое движение в конкретную команду.

CommandMove

public class CommandMove : ICommand{    public CommandMove(GameObject obj, Vector3 direction)    {        mGameObject = obj;        mDirection = direction;    }    public void Execute()    {        mGameObject.transform.position += mDirection;    }    public void ExecuteUndo()    {        mGameObject.transform.position -= mDirection;    }    GameObject mGameObject;    Vector3 mDirection;}

CommandMoveTo

public class CommandMoveTo : ICommand{    public CommandMoveTo(GameManager manager, Vector3 startPos, Vector3 destPos)    {        mGameManager = manager;        mDestination = destPos;        mStartPosition = startPos;    }    public void Execute()    {        mGameManager.MoveTo(mDestination);    }    public void ExecuteUndo()    {        mGameManager.MoveTo(mStartPosition);    }    GameManager mGameManager;    Vector3 mDestination;    Vector3 mStartPosition;}

Обратите внимание, как реализован метод ExecuteUndo. Он просто делает обратное тому, что делает метод Execute.

Класс Invoker

Теперь нам нужно реализовать класс Invoker. Помните, что Invoker это класс, который содержит все команды. Также помните, что для работы Undo нам нужно будет реализовать структуру данных типа Last In First Out (LIFO).

Что такое LIFO? Как мы можем реализовать LIFO? Представляю вам структуру данных Stack.

C# предоставляет особый тип коллекции, в которой элементы хранятся в стиле LIFO (Last In First Out). Эта коллекция включает в себя общий и не общий стек. Он предоставляет метод Push() для добавления значения в верх (в качестве последнего), метод Pop() для удаления верхнего (или последнего) значения и метод Peek() для получения верхнего значения.

Теперь мы реализуем класс Invoker, который будет содержать стек команд.

public class Invoker{    public Invoker()    {        mCommands = new Stack<ICommand>();    }    public void Execute(ICommand command)    {        if (command != null)        {            mCommands.Push(command);            mCommands.Peek().Execute();        }    }    public void Undo()    {        if(mCommands.Count > 0)        {            mCommands.Peek().ExecuteUndo();            mCommands.Pop();        }    }    Stack<ICommand> mCommands;}

Обратите внимание, как методы Execute и Undo реализуются инвокером. При вызове метода Execute инвокер помещает команду в стек, вызывая метод Push и затем выполняет метод Execute команды. Команда сверху стека получается с помощью метода Peek. Точно так же и при вызове

Undo инвокера вызывает метод ExecuteUndo команды, получая верхнюю команду из стека (используя метод Peek). После этого Invoker удаляет верхнюю команду, с помощью метода Pop.

Теперь мы готовы готовы использовать Invoker и команды. Для этого мы сначала добавим новую переменную для объекта Invoker в наш класс GameManager.

private Invoker mInvoker;

Дальше нам нужно инициализировать объект mInvoker в методе Start нашего скрипта GameManager.

mInvoker = new Invoker();

Undo

Вызовем отмену мы будем нажатием клавиши U. Добавим следующий код в метод Update.

// Undo     if (Input.GetKeyDown(KeyCode.U))    {        mInvoker.Undo();    }

Использование команд

Теперь мы изменим метод Update в соответствии с реализацией паттерна Команда.

void Update(){    Vector3 dir = Vector3.zero;    if (Input.GetKeyDown(KeyCode.UpArrow))        dir.z = 1.0f;    else if (Input.GetKeyDown(KeyCode.DownArrow))        dir.z = -1.0f;    else if (Input.GetKeyDown(KeyCode.LeftArrow))        dir.x = -1.0f;    else if (Input.GetKeyDown(KeyCode.RightArrow))        dir.x = 1.0f;    if (dir != Vector3.zero)    {        //Using command pattern implementation.        ICommand move = new CommandMove(mPlayer, dir);        mInvoker.Execute(move);    }    var clickPoint = GetClickPosition();    //Using command pattern right click moveto.    if (clickPoint != null)    {        CommandMoveTo moveto = new CommandMoveTo(            this,             mPlayer.transform.position,             clickPoint.Value);        mInvoker.Execute(moveto);    }    // Undo     if (Input.GetKeyDown(KeyCode.U))    {        mInvoker.Undo();    }}

Нажмите кнопку Play и посмотрите, что получилось. Нажимайте клавиши со стрелками (Up, Down, Left и Right), чтобы увидеть движение игрока, и клавишу u для отмены в обратном порядке.

Заключение

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

Листинг скрипта для Unity

using System.Collections;using System.Collections.Generic;using UnityEngine;public class GameManager : MonoBehaviour{    public interface ICommand    {        void Execute();        void ExecuteUndo();    }    public class CommandMove : ICommand    {        public CommandMove(GameObject obj, Vector3 direction)        {            mGameObject = obj;            mDirection = direction;        }        public void Execute()        {            mGameObject.transform.position += mDirection;        }        public void ExecuteUndo()        {            mGameObject.transform.position -= mDirection;        }        GameObject mGameObject;        Vector3 mDirection;    }    public class Invoker    {        public Invoker()        {            mCommands = new Stack<ICommand>();        }        public void Execute(ICommand command)        {            if (command != null)            {                mCommands.Push(command);                mCommands.Peek().Execute();            }        }        public void Undo()        {            if (mCommands.Count > 0)            {                mCommands.Peek().ExecuteUndo();                mCommands.Pop();            }        }        Stack<ICommand> mCommands;    }    public GameObject mPlayer;    private Invoker mInvoker;    public class CommandMoveTo : ICommand    {        public CommandMoveTo(GameManager manager, Vector3 startPos, Vector3 destPos)        {            mGameManager = manager;            mDestination = destPos;            mStartPosition = startPos;        }        public void Execute()        {            mGameManager.MoveTo(mDestination);        }        public void ExecuteUndo()        {            mGameManager.MoveTo(mStartPosition);        }        GameManager mGameManager;        Vector3 mDestination;        Vector3 mStartPosition;    }    public IEnumerator MoveToInSeconds(GameObject objectToMove, Vector3 end, float seconds)    {        float elapsedTime = 0;        Vector3 startingPos = objectToMove.transform.position;        end.y = startingPos.y;        while (elapsedTime < seconds)        {            objectToMove.transform.position = Vector3.Lerp(startingPos, end, (elapsedTime / seconds));            elapsedTime += Time.deltaTime;            yield return null;        }        objectToMove.transform.position = end;    }    public Vector3? GetClickPosition()    {        if (Input.GetMouseButtonDown(1))        {            RaycastHit hitInfo;            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);            if (Physics.Raycast(ray, out hitInfo))            {                //Debug.Log("Tag = " + hitInfo.collider.gameObject.tag);                return hitInfo.point;            }        }        return null;    }    // Start is called before the first frame update    void Start()    {        mInvoker = new Invoker();    }    // Update is called once per frame    void Update()    {        Vector3 dir = Vector3.zero;        if (Input.GetKeyDown(KeyCode.UpArrow))            dir.z = 1.0f;        else if (Input.GetKeyDown(KeyCode.DownArrow))            dir.z = -1.0f;        else if (Input.GetKeyDown(KeyCode.LeftArrow))            dir.x = -1.0f;        else if (Input.GetKeyDown(KeyCode.RightArrow))            dir.x = 1.0f;        if (dir != Vector3.zero)        {            //----------------------------------------------------//            //Using normal implementation.            //mPlayer.transform.position += dir;            //----------------------------------------------------//            //----------------------------------------------------//            //Using command pattern implementation.            ICommand move = new CommandMove(mPlayer, dir);            mInvoker.Execute(move);            //----------------------------------------------------//        }        var clickPoint = GetClickPosition();        //----------------------------------------------------//        //Using normal implementation for right click moveto.        //if (clickPoint != null)        //{        //    IEnumerator moveto = MoveToInSeconds(mPlayer, clickPoint.Value, 0.5f);        //    StartCoroutine(moveto);        //}        //----------------------------------------------------//        //----------------------------------------------------//        //Using command pattern right click moveto.        if (clickPoint != null)        {            CommandMoveTo moveto = new CommandMoveTo(this, mPlayer.transform.position, clickPoint.Value);            mInvoker.Execute(moveto);        }        //----------------------------------------------------//        //----------------------------------------------------//        // Undo         if (Input.GetKeyDown(KeyCode.U))        {            mInvoker.Undo();        }        //----------------------------------------------------//    }    public void MoveTo(Vector3 pt)    {        IEnumerator moveto = MoveToInSeconds(mPlayer, pt, 0.5f);        StartCoroutine(moveto);    }}

Ссылки

Wikidepia Design Patterns

Wikipedia Command Design Pattern

Refactoring Guru

Game Programming Patterns

Design Patterns in Game Programming


Узнать подробнее о курсе "Unity Game Developer. Professional".

Посмотреть открытый урок на тему "Продвинутый искусственный интеллект врагов в шутерах".


ЗАБРАТЬ СКИДКУ

Подробнее..

Strategy Design Pattern

13.04.2021 22:15:19 | Автор: admin

Всем привет, друзья. Меня зовут Alex, я профессиональный разработчик и создатель программных продуктов в веб индустрии. Много лет изучаю языки, делюсь опытом с другими.

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

В чем суть?

Design patter Strategy или шаблон проектирования Стратегия относится к поведенческим шаблонам проектирования. Его задача - выделить схожие алгоритмы, решающие конкретную задачу. Реализация алгоритмов выносится в отдельные классы и предоставляется возможность выбирать алгоритмы во время выполнения программы.

Шаблон дает возможность в процессе выполнения выбрать стратегию (алгоритм, инструмент, подход) решения задачи.

В чем проблема?

Рассмотрим задачи, при решении которых можно применять такой подход.

Представьте, что перед вами стоит задача написать веб-портал по поиску недвижимости. MVP (Minimum ViableProduct) или минимально работающий продукт был спроектирован и приоритизирован вашей командой Product Managerов и на портале должен появиться функционал для покупателей квартир. То есть целевые пользователи вашего продукта в первую очередь - это те, кто ищет себе новое жилье для покупки. Одной из самых востребованных функций должна быть возможность:

  • Выбрать область на карте, где покупатель желает приобрести жилье

  • И указать ценовой диапазон цен на квартиры для фильтрации.

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

Но тут приходят к вам Product Manager'ы и говорят, что нужно добавить возможность искать и отображать недвижимость, которая сдается в аренду. У нас появляется еще один тип пользователя - арендаторы. Для арендаторов не так важно показывать фильтры по цене, им важно состояние квартиры, поэтому нужно отображать фотографии арендуемых квартир.

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

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

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

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

  • Основной алгоритм поиска квартир был реализован в одном супер-классе

  • Алгоритм выбора и отображения элементов интерфейса был реализован в одном супер-классе

  • Изменения в этих классах, сделанные разными программистами, приводили к конфликтам и необходимости регрессивного тестирования

  • Релизы продукта затягивались, время на разработку нового функционала увеличилась в несколько раз

  • Больше времени стало уходить на разработку, тестирование, появилось множество багов.

Супер-класс с единым методом реализации алгоритма.Супер-класс с единым методом реализации алгоритма.

Какое решение?

В данном примере мы имеем несколько алгоритмов для одной функции:

  • Поиск квартир с продажей

  • Поиск квартир в аренду

  • Отображение или нет различных наборов фильтров

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

Шаблон проектирования Стратегия - решает такую задачу. Онпредлагает выделить семейство похожих алгоритмов, вынести их в отдельные классы. Это позволит без проблем изменять нужный алгоритм, расширять его, сводя к минимум конфликты разработки, зависимости от других классов и функционала. Вместо того, чтобы реализовывать алгоритм в едином классе, наш класс будет работать с объектами классов-стратегиями через объект-контекста и в нужным момент делегировать работу нужному объекту. Для смены алгоритма достаточно в нужным момент подставить в контекст нужный объект-стратегию.

Чтобы работа нашего класса была одинаковой для разного поведения, у объектов-стратегии должен быть общий интерфейс. Используя такой интерфейс вы делаете независимым наш класс-контекста от классов-стратегий.

Диаграмма классов шаблона StrategyДиаграмма классов шаблона Strategy

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

В классах-стратегиях, реализующих поиск объектов недвижимости, будет определен только один метод doSearch(filters), принимающий в качестве аргумента фильтры, по которым будет совершен поиск конкретных объектов с использованием конкретного алгоритма.

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

Задача контроллера определить класс-стратегию и запросить у класса-контекста данные для отображения, передав ему известный набор фильтров. Класс-контекст в этой схеме - это класс, которые реализует метод поиска квартир по заданным фильтрам. На диаграмме классов выше мы видим, что класс контекста определяет метод getData, и принимает аргументы filters. У него должен быть конструктор, принимающий активный в данный момент объект-стратегии и сеттерsetStrategy, устанавливающий активную стратегию. Такой метод пригодится для случая, когда пользователь меняет тип искомого объекта, например, он ищет недвижимость на продажу и хочет снять квартиру.

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

Ниже рассмотрим пример, как решается описанная задача на языке GOlang. Первое что сделаем - определим интерфейс с методом doSearch:

Strategy.goStrategy.go

Данный метод определяет общее поведение для конкретных алгоритмов, реализующих разные стратегии. Метод может принимать различные аргументы, позволяющие реализовать ветвления в ваших алгоритмах. В примера я передаю пользовательские фильтры с типом Map.

Для реализации конкретных алгоритмов создаем два файла. В каждом файле определяется свой определяемый тип с базовым типом struct, реализующие интерфейс Strategy. Соответственно, в методы, определяемые интерфейсом для каждого алгоритма, будут передаваться пользовательские фильтры. Реализации выглядит следующим образом:

FirstAlgorithm.goFirstAlgorithm.goSecondAlgorithm.goSecondAlgorithm.go

Посмотрим на нашу диаграмму классов. Нам осталось реализовать класс-контекста и клиентский код вызова конкретных алгоритмов в нужным момент. Как это сделать? Для создания слоя класса-контекста реализуем исходник, реализующий:

  • определяемый тип в базовым типом struct

  • функцию initStrategy, инициализирующий стратегию по-умолчанию и пользовательские фильтры

  • метод типа struct setStrategy, устанавливающий активную стратегию

  • и функция getData, вызывающий конкретную стратегию и возвращаемый данные для показа пользователю.

Context.goContext.go

Последний шаг - посмотрим на клиентский код. Сначала инициализируется стратегия по-умолчанию, получаем данные из этой стратегии. Затем выставляем новую активную стратегию и повторно вызываем функцию получения данных getData. Как видим, каждый раз вызывается одна и та же функция файла-контекста (или класса-контекста в диаграмме классов) для получения данных рендеринга, отображаемых в дальнейшем пользователю. Для простоты я вывожу отладочную информацию в консоль, чтобы убедиться, что работают разные алгоритмы при вызовах. Вот код:

Client.goClient.go

Вот вывод такого подхода:

First implements strategy map[role:1]

Second implements strategy map[role:2]

Как видите, мы можем управлять вызовом разных алгоритмов в зависимости от контекста и пользовательских фильтров. Алгоритмы могут создавать ветвления в зависимости от входных фильтров и других параметров, переданных из клиентского кода в методы, реализующие конкретные алгоритмы. Вот такой интересный пример.

Объектно-ориентированный подход можно посмотреть. например, в этом курсе. Там показан пример на PHP.

Когда применять?

Напоследок поговорим когда применяется шаблон Strategy?

  1. У вас есть множество похожих реализаций отличающихся незначительным поведением. Можно вынести отличающее поведение в классы-стратегии, а повторяющий код свести к единому классу-контекста.

  2. Ваш алгоритм реализован в супер-классе с множественными условными операторами. Выделите блоки условных операторов в отдельные классы-стратегии, а управление вызовов нужных доверьте классу-контекста.

  3. Конкретные стратегии позволяют инкапсулировать алгоритмы в своих конкретных классах. Используйте этот подход для снижения зависимостей от других классов.

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

Подведем итог

Друзья, мы познакомились с поведенческим шаблоном проектирования Strategy. Шаблон используется для выделения схожих алгоритмов, решающих конкретную задачу. Посмотрели с вами реализацию на языке GOlang, ознакомились в возможностями подхода и разобрали когда его лучше применять.

Рад был с вами пообщаться, Alex Versus. Успехов!

Подробнее..

Factory Method Pattern

09.05.2021 20:20:27 | Автор: admin

Привет, друзья. С вами Alex Versus.

Ранее мы говорили про шаблоны проектирования Одиночка и Стратегия, про тонкости реализации на языке Golang.

Сегодня расскажу про Фабричный метод.

В чем суть?

Фабричный метод (Factory method) так же известный как Виртуальный конструктор(Virtual Constructor) - пораждающий шаблон проектирования, определяющий общий интерфес создания объектов в родительском классе и позволяющий изменять создаваемые объекты в дочерних классах.

Шаблон позволяет классу делегировать создание объектов подклассам. Используется, когда:

  1. Классу заранее неизвестно, объекты каких подклассов ему нужно создать.

  2. Обязанности делегируются подклассу, а знания о том, какой подкласс принимает эти обязанности, локализованы.

  3. Создаваемые объекты родительского класса специализируются подклассами.

Какую задачу решает?

Представьте, что вы создали программу управления доставкой еды. В программе в качестве единственного средства доставки используется электро-самокат. Ваши курьеры на электро-самокатах развозят еду из пункта А в пункт Б. Все просто.

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

Вы обнаруживаете, что большая часть ваших сущностей в программе сильно связаны с объектом Самокат и чтобы заставить вашу программу работать с другими способами доставки, вам придется добавить связи в 80% вашей кодовой базы и так повторить для каждого нового транспорта. Знакомая ситуация?

В итоге вы получите ужасающий код, наполненный условными операторами, которые выполняют то или иное действие, в зависимости от транспорта.

И какое решение?

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

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

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

Посмотрим на диаграмму классов такого подхода.

Диаграмма классов Factory MethodДиаграмма классов Factory Method

Реализация на Golang

Пример реализации на PHP, можно изучить тут. Так как в Golang отсутствуют возможности ООП, такие как классы и наследование, то реализовать в классическом виде этот шаблон невозможно. Несмотря на это, мы можем реализовать базовую версию шаблона - Простая фабрика.

В нашем примере есть файл iTransport.go, который определяет методы создаваемых транспортных средств для доставки еды. Сущность транспорта будем хранить в структуре (struct), которая применяет интерфейс iTransport.

Так же реализуем файл Factory.go, который представляет фабрику создания нужных объектов. Клиентский код реализован в файле main.go. Вместо прямого создания конкретных объектов транспорта клиентский код будет использовать для этого метод фабрики getTransport(t string), передавая нужный тип объекта в виде аргумента функции.

Когда применять?

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

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

Какие преимущества?

  1. Избавляет слой создания объектов от конкретных классов продуктов. Выделяет код производства продуктов в одно место, упрощая поддержку кода.

  2. Упрощает добавление новых продуктов в программу.

  3. Реализует принцип открытости/закрытости (англ. openclosed principle, OCP) принцип ООП, устанавливающий следующее положение: программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения

Какие недостатки?

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

Итог

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

Рад был поделиться материалом, Alex Versus. Публикация на английском.
Всем удачи!

Подробнее..

Prototype Design Pattern в Golang

24.05.2021 18:21:26 | Автор: admin

Привет друзья! С вами Алекс и я продолжаю серию статей, посвящённых применению шаблонов проектирования в языке Golang.

Интересно получать обратную связь от вас, понимать на сколько применима данная область знаний в мире языка Golang. Ранее уже рассмотрели шаблоны: Simple Factory, Singleton и Strategy. Сегодня хочу рассмотреть еще один шаблон проектирования - Prototype.

Для чего нужен?

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

Какую проблему решает?

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

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

Какое решение?

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

Реализация этого метода в разных классах очень схожа. Метод создаёт новый объект текущего класса и копирует в него значения всех полей собственного объекта. Так получится скопировать даже приватные поля, так как большинство языков программирования разрешает доступ к приватным полям любого объекта текущего класса. Объект, который копируют, называется прототипом, отсюда и название шаблона. Когда объекты программы содержат сотни полей и тысячи возможных конфигураций, прототипы могут служить своеобразной альтернативой созданию подклассов. Шаблон прототип должен копировать объекты любой сложности без привязки к их конкретным классам.

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

Диаграмма классов

Prototype Class DiagramPrototype Class Diagram

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

Как реализовать?

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

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

В какой-то момент потребовалась панель администрирования рубрикатора, чтобы копировать разделы в раздел и рубрики со всеми свойствами товаров. Например, в разделе одежды нужно быстро уметь клонировать рубрики мужские, женские, детские шорты и т.д.

Каждую рубрика, как конечный элемент рубрикатора, может быть представлен интерфейсом prototype, который объявляет функцию clone. За основу конкретных прототипов рубрики и раздела мы берем тип struct, которые реализуют функции show и clone интерфейса prototype.

Итак, реализуем интерфейс прототипа. Далее мы реализуем конкретный прототип directory, который реализует интерфейс prototype представляет раздел рубрикатора. И конкретный прототип для рубрики. Обе структуру реализуют две функции show, которая отвечает за отображение конкретного контента ноды и clone для копирования текущего объекта. Функция clone в качестве единственного параметра принимает аргумент, ссылающийся на тип указателя на структуру конкретного прототипа - это либо рубрика, либо директория. И возвращает указатель на поле структуры, добавляя к наименованию поля _clone.

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

Open directory 2  Directory 2    Directory 1        category 1    category 2    category 3Clone and open directory 2  Directory 2_clone    Directory 1_clone        category 1_clone    category 2_clone    category 3_clone

Когда применять?

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

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

Итог

Друзья, шаблон Prototype предлагает:

  • Удобную концепцию для создания копий объектов.

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

  • В объектных языках позволяет избежать наследования создателя объекта в клиентском приложении, как это делает паттерн abstract factory, например.

Кстати, друзья, вот тут можно посмотреть результаты опроса читателей хабра. 63% опрошенных считают, что применение шаблонов проектирования в Golang - это зло. Связано, скорее всего, с тем, что язык Golang процедурный и ему чужды подходы объектно-ориентированных языков. Но рассматривать реализации и применение шаблонов стоит, так как это позволяет больше их понимать и периодически применять для решения тех или иных задач. Каждый подход требует, конечно, дискуссии и разумного применения.

Друзья, рад был поделиться темой, Алекс. На английском статью можно найти тут.
Удачи!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru