Ссылка на один и тот же объект в нескольких коллекциях по интерфейсу
Предупреждение: этот вопрос использует аналогию с 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 на эту новую "вещь".. и затем вставить код для обработки этой новой "вещи" в своей метод обновления.