Ссылка на один и тот же объект в нескольких коллекциях по интерфейсу

Предупреждение: этот вопрос использует аналогию с RPG в качестве примера.

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

Теперь все эти элементы имеют одну, но несколько ролей: например, Союзник (который является Истребителем через прямое наследование) способен двигаться на поле битвы, выдавать команды или быть целенаправленными с помощью ennemies.

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

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

Пример кода:

public class Ally : Fighter, IMovable, IActor, ITarget
{
    // IMovable implementation
    public void Move(...) { ... }

    // IActor implementation
    public bool HasInitiative { get { ... } }

    public ICommand IssueCommand(...) { ... }

    // ITarget implementation
    public void OnTarget(...) { ... }

    // other code ...
}

public class MightySword : ITarget, ILiftable, IUsable
{
    // ITarget implementation
    public void OnTarget(...) { ... }

    // ... other interface implementation

    // other code ...
}

Теперь мой вопрос следующий: Как должен мой класс Battlefield ссылаться на все эти объекты? Я придумал два возможных решения, но на данный момент неясно, какой из них лучше, и нужно ли найти лучшие решения.

Решение A:

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

public class Battlefield : ...
{
   IList<IMovable> movables;
   IList<ITarget> targets;
   IList<IActor> actors;
   // ... and so on
}

и метод Battlefield.Update может выглядеть следующим образом:

Update(...) 
{
  // Say every actor needs to target somebody else
  // it has nothing to do in the Update method,
  // but this is an example
  foreach (IActor actor in actors) 
  {
    if (actor.HasInitiative) 
    {
      ITarget target = targets[actor.TargetIndex];
      target.OnTarget(actor.IssueCommand(...));
    }
  }
}

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

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

Решение B:

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

Update(...)
{
    IList<IActor> actors = elements.OfType<Actor>();
    foreach (IActor actor in actors) 
    {
     ITarget target = elements.OfType<Target>()[actor.TargetIndex];
     target.OnTarget(actor.IssueCommand(...));
    }
}

Там. Больше нет избыточности, но мне не нравится использование конструкции "typeof", и я вообще стараюсь ее избегать. Очевидно, что это хуже, чем время выполнения.

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

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

Ответ 1

Решение A, но не отключайте запрос всего списка экземпляров сразу, если есть причина для этого.

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

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

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

Как и производительность, память профиля перед проектированием вокруг него. Если вы используете Visual Studio, у вас есть отличные инструменты, доступные вам для этого. И, по-видимому, вы не копируете эти экземпляры, поэтому накладные расходы минимальны.

Гибридный подход также может иметь смысл.

  • Добавить/удалить из одной коллекции.
  • Экспортировать коллекции коллекции и фильтровать только для чтения определенного типа.
  • Только (повторно) создайте те отфильтрованные коллекции, когда это необходимо.

public class Battlefield
{
    private readonly IList<IBattleElement> _all = new List<IBattleElement>();
    private IReadOnlyList<IActor> _actors;
    private bool _isDirty;

    /// <summary>
    ///     Gets a list of all elements.
    /// </summary>
    public IReadOnlyList<IBattleElement> All
    {
        get { return _all; }
    }

    /// <summary>
    ///     Gets a filtered list of elements that are <c>IActor</c>.
    /// </summary>
    public IReadOnlyList<IActor> Actors
    {
        get
        {
            if( _isDirty || _actors == null )
            {
                _actors = All.OfType<IActor>().ToList();
            }

            return _actors;
        }
    }

    /// <summary>
    ///     The one and only way to add new elements. This could be made thread-safe if needed.
    /// </summary>
    /// <param name="element"></param>
    public void AddElement( IBattleElement element )
    {
        _all.Add( element );
        _isDirty = true;
    }
}

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

Не глупо вообще.

Ответ 2

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

Кстати... вопрос вроде этого больше основан на мнениях, поэтому я не думаю, что есть правильный или неправильный ответ. Например, я мог бы настроить решение A для реализации IDictionary вместо IList, и вам не нужно беспокоиться об обслуживании индекса!

Ответ 3

Я бы создал один последний интерфейс. IBattleFieldObject, поскольку все они являются общими. Затем поместите их все в список через конструктор для Injection Dependency. Затем в вашем методе обновления я бы использовал лямбда, чтобы извлечь все IActor через linq. Затем выполните мои действия, относящиеся к IActor. Кроме того, в разделе "Обновление" вы можете обрабатывать любые другие интерфейсы, которые необходимо использовать для имитации поля боя.

var IActors = (from list in battlefieldobjects where list is IActor select list).ToList();

Или вы можете сделать расширение:

public static List<IActor> GetActors(this List<IBattleFieldObject> list)
{
    return list.Where(t => t is IActor).ToList();
}

Использование: battlefieldobjects.GetActors();

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

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