Проблема со слоем реализации, имеющим подпись, отличную от уровня абстракции

#c# #abstraction

#c# #абстракция

Вопрос:

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

Допустим, я разрабатываю библиотеку (C #), которая не зависит от того, в какой системе она используется. Итак, у меня есть следующие классы:

 public interface IAction
{
    void Execute();
}

public class Trigger
{
    private List<IAction> actions;

    public void CheckConditions()
    {
        if (AllConditionsMet())
        {
           ExecuteAllActions();
        }
    }

    private void ExecuteAllActions()
    {
        foreach (IAction action in actions)
        {
            action.Execute();
        }
    }

    protected abstract bool AllConditionsMet();
}

public class BigBulkySystem
{
    private List<Trigger> triggers;

    public void CheckAllTriggers()
    {
        foreach (Trigger trigger in triggers)
        {
            trigger.CheckConditions();
        }
    }
}
  

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

Проблема, с которой я сталкиваюсь сейчас, заключается в том, что я хочу реализовать уровень, специально разработанный для Unity. В Unity вы используете сопрограммы, чтобы иметь возможность подождать несколько кадров, прежде чем продолжить выполнение метода. Это именно то, что я хотел бы для триггера.Метод ExecuteAllActions. Я хочу дождаться завершения выполнения каждого действия, прежде чем переходить к следующему. Чтобы сделать это, мне нужно было бы иметь разные подписи для триггера и IAction. Вот как это было бы в Unity:

 public interface IAction
{
    IEnumerator Execute();
}

public class Trigger
{
    private List<IAction> actions;

    public void CheckConditions()
    {
        if (AllConditionsMet())
        {
            StartCoroutine(ExecuteAllActions());
        }
     }

    public IEnunmerator ExecuteAllActions()
    {
        foreach (IAction action in actions)
        {
            yield return StartCoroutine(action.Execute());
        }
    }
 }
  

Обратите внимание, что пара методов теперь возвращает IEnumerator вместо void , именно так вы определяете сопрограмму в Unity.

Вот пример действия Unity, которое перемещает объект player с позиции (0,0,0) на (1,1,1) за 5 секунд после 3-секундной задержки:

 public class MovePlayerAction : IAction
{
    public GameObject playerObject;

    public IEnumerator Execute()
    {
        yield return StartCoroutine(WaitFor(3)); // Wait 3 secs

        float animDuration = 5.0f;
        Vector3 initialPos = Vector3.zero;
        Vector3 finalPos = Vector3.one;
        float t = 0;
        while (t < animDuration)
        {
            playerObject.transform.position = Vector3.Lerp(initialPos, finalPos, t / animDuration); // Lerp between initial and final
            yield return null; // Wait a frame
            t  = Time.deltaTime;
        }
    }

    private IEnumerator WaitFor(float secs)
    {
        float t = 0;
        while (t < secs)
        {
            yield return null; // Wait a single frame
            t  = Time.deltaTime;
        }
    }
}
  

Итак, мой вопрос таков: есть ли способ спроектировать систему таким образом, чтобы я мог сохранить уровень, отличный от Unity, свободным от IEnumerator, но при этом использовать их на уровне Unity?

У меня такое ощущение, что существует какой-то средний уровень, но даже в этом случае я не мог понять, как IAction может иметь одновременно void Execute() и IEnumerator Execute() .

Спасибо!

Комментарии:

1. Я немного смущен public IEnunmerator ExecuteAllActions() . Вы действительно должны возвращать IEnumerator поверх IEnumerators? И что именно должна была бы возвращать реализация Execute в качестве своего IEnumerator?

2. Да, это немного сбивает с толку, но именно так работают сопрограммы в Unity. Нет, вы ничего не возвращаете. Вы пишете метод итератора, о вызове которого Unity заботится на этапе обновления. В примере вы видите сопрограмму (ExecuteAllActions), которая последовательно выполняет другие сопрограммы (каждое действие).

3. Я имею в виду, что если я напишу действие, которое реализует IAction (версию Unity), что должно быть возвращено из Execute метода этого класса? Он должен возвращать IEnumerator , иначе он не будет компилироваться!

4. Нет, IEnumerator — это особый случай в C #, где вы можете определить метод итератора вместо возврата IEnumerator . Т.е. открытый класс TestAction : IAction { public IEnumerator Execute() { yield null; yield break; } } будет работать, даже если он не возвращает IEnumerator. Если только я не неправильно понял, что вы только что сказали (снова). Известно ли вам о методах итератора C #? codeproject.com/Tips/359873/Csharp-Iterator-Pattern-demystified

5. Я знаю о них (хотя обычно я вижу их с помощью IEnumerable, а не IEnumerator). Так что, возможно, я задал неправильный вопрос. В этом случае они только когда-либо yield break , или сначала они выдают что-нибудь еще?

Ответ №1:

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

 public interface IAction
{
    void Execute();
}

public interface IUnityAction
{
    IEnumerator Execute();
}

public class UnityActionAdapter : IUnityAction
{
    private readonly IAction Action;

    public UnityActionAdapter(IAction action)
    {
        Action = action;
    }

    public IEnumerator Execute()
    {
        Action.Execute();
        yield break; //Or whatever you need to yield here
    }
}
  

Обновить

В вашем случае, похоже, вы бы адаптировались в другом направлении, поэтому:

 public class UnityActionAdapter : IAction
{
    private readonly IUnityAction UnityAction;

    public UnityActionAdapter(IAction unityAction)
    {
        UnityAction = action;
    }

    public void Execute()
    {
        StartCoroutine(UnityAction.Execute()); //If I'm correctly understanding how you'd execute an action here
    }
}
  

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

Это предполагает, что вам действительно нужно поддерживать IEnumerator форму в интерфейсе на вашем уровне Unity. Возможно, вы сможете достичь того же самого без необходимости в адаптерах, выполнив что-то вроде:

 public class MovePlayerAction : IAction
{
    public GameObject playerObject;

    private IEnumerator Coroutine()
    {
        yield return StartCoroutine(WaitFor(3)); // Wait 3 secs

        float animDuration = 5.0f;
        Vector3 initialPos = Vector3.zero;
        Vector3 finalPos = Vector3.one;
        float t = 0;
        while (t < animDuration)
        {
            playerObject.transform.position = Vector3.Lerp(initialPos, finalPos, t / animDuration); // Lerp between initial and final
            yield return null; // Wait a frame
            t  = Time.deltaTime;
        }
    }

    public void Execute()
    {
        StartCoroutine(Coroutine());
    }

    private IEnumerator WaitFor(float secs)
    {
        float t = 0;
        while (t < secs)
        {
            yield return null; // Wait a single frame
            t  = Time.deltaTime;
        }
    }
}
  

Комментарии:

1. Можете ли вы показать мне, где класс MovePlayerAction вписался бы в этот дизайн?

2. @pek обновлен несколькими опциями

3. Ага! Закройте, но вы упускаете одну очень важную часть: класс триггера Unity также имеет сигнатуру, отличную от триггера, отличного от Unity. И это самое сложное, потому что метод ExecuteAllActions действительно должен быть методом итератора, чтобы дождаться выполнения каждого действия перед продолжением. Также обратите внимание, что он вызывает метод выполнения IEnumerator вместо метода выполнения void, потому что в противном случае он не может запустить вспомогательную сопрограмму.

4. @pek Создайте интерфейс (или абстрактный базовый класс) также для триггера и внедрите в UnityTrigger который реализует его из уровня unity. Если ExecuteAllActions необходимо быть общедоступным, то в версии Unity переместите итератор в частный метод и попросите ExecuteAllActions метод просто запустить сопрограмму, указав в качестве параметра метод iterator.

5. Хммм… Я считаю, что вы правы. Я рассмотрю это. Спасибо.

Ответ №2:

Что, если вы используете параллельную библиотеку задач?

Пример кода триггера, в котором я вижу, что мы можем это реализовать (учитывая ваш код, отличный от IEnumerator):

 public class Trigger
    {
        private readonly List<Task> TaskActions = new List<Task>();

        public void AddAction(IAction action)
        {
            TaskActions.Add(new Task(()=>{action.Execute();})
        }

        public void CheckConditions()
        {
            if (AllConditionsMet())
            {
                ExecuteAllActions();
            }
        }

        private void ExecuteAllActions()
        {
            foreach(Task task in TaskActions)
            {
                task.Start();
            }
            Task.WaitAll(TaskActions);
        }

        protected abstract bool AllConditionsMet();
    }
  

С уважением