Вариант номер 3 и плюс вариант номер 2. Это стандартный подход с минимальными затратами и без дублирования кода.
Еще можно к варианту 2 добавить в каждый класс списки необщих действий (методов), которые можно совершать для объекта этого класса. Тогда даже who is it не понадобится, просто создавай контекстное меню по списку.
В варианте 3 в классе Item нужно создать только общие для всех предметов методы и свойства, тогда никаких копипастов не будет. Такими методами могут быть Take и ClearDust :-). В общем, там есть где развернуться. Даже такой метод, как SetEnabled (может, подразумевалось Turn(On/Off)?) должен быть общим. Его реализацией по умолчанию в классе Item я бы сделал ответ "Действие неприменимо".
Кстати, для телека можно GetContent сделать, но тогда он сломается :-(. Так что общих методов наберётся достаточно много.
Вот (не)большой пример:
using System.Collections;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace ActionListTest;
// этот класс нужен, можно добавить свои свойства или удалить имеющиеся
[AttributeUsage(AttributeTargets.Method)]
public class ActionItemAttribute : Attribute {
public string ActionName { get; set; }
public string ActionDescription { get; set; }
}
// если сделать реализацию немножко по-другому, то без этого класса можно обойтись
public class ActionItem {
public IList<CustomAttributeNamedArgument> Attributes { get; init; }
public string MethodName { get; init; }
public ActionItem(string mn, IList<CustomAttributeNamedArgument> il) { MethodName = mn; Attributes = il; }
}
class Item {
// это нужно для построения спика действий
protected static List<ActionItem> GetActionList(Type T) =>
T.GetMethods().Where(w => w.CustomAttributes
.Where(w => w.AttributeType == typeof(ActionItemAttribute)).Any())
.Select(s => new ActionItem(s.Name, s.CustomAttributes
.Single(s => s.AttributeType == typeof(ActionItemAttribute)).NamedArguments))
.ToList();
public string Name { get; init; }
public Item(string nm) => Name = nm;
// это вспомогательная функция, можно и без неё
protected string GetActionDescription([CallerMemberName] string callingMethod = "")
=> (GetType().GetMethod(callingMethod)!.GetCustomAttribute(typeof(ActionItemAttribute)) as ActionItemAttribute)
?.ActionDescription!;
// это нужно
public List<ActionItem> ActionableItems {
get => GetType().GetField("_sactionlist", BindingFlags.Static | BindingFlags.NonPublic)
.GetValue(this) as List<ActionItem>;
}
// это нужно
public object Act(string action) {
Console.Write($" {((ActionItemAttribute)Attribute
.GetCustomAttribute(GetType().GetMethod(action)
, typeof(ActionItemAttribute), true)).ActionName}: ");
if (GetType().GetMethod(action).ReturnType != typeof(void))
return GetType().GetMethod(action).Invoke(this, new object[] { });
else { GetType().GetMethod(action).Invoke(this, new object[] { }); return null; }
}
// каждое действие должно быть помечено таки атрибутом, он может быть и пустой,
// если дополнительная информация не нужна
[ActionItem(ActionName = "Взять", ActionDescription = "Взять предмет")]
public virtual void Take() => Console.WriteLine("Не забудь вернуть обратно.");
[ActionItem(ActionName = "Вытащить содержимое", ActionDescription = "Вытащить содержимое из предмета")]
public virtual object GetContent() { Console.WriteLine("Аааа, ломать не нужно!!!"); return null; }
}
class WindowItem : Item {
// это нужно, одинаковое во всех классах - без повторения не получится
static List<ActionItem> _sactionlist = GetActionList(MethodBase.GetCurrentMethod().DeclaringType);
public WindowItem(string name) : base(name) { }
[ActionItem(ActionName = "Открыть", ActionDescription = "Открыть окно")]
public void Open() => Console.WriteLine(GetActionDescription());
[ActionItem(ActionName = "Закрыть", ActionDescription = "Закрыть окно")]
public void Close() => Console.WriteLine(GetActionDescription());
[ActionItem(ActionName = "Взять", ActionDescription = "Взять предмет")]
public override void Take() => Console.WriteLine("Ага, попробуй возьми.");
}
class Cloth : Item {
static List<ActionItem> _sactionlist = GetActionList(MethodBase.GetCurrentMethod().DeclaringType);
public Cloth(string name) : base(name) { }
public void Wash() => Console.WriteLine("Постирать тряпку.");
}
class Box : Item {
static List<ActionItem> _sactionlist = GetActionList(MethodBase.GetCurrentMethod().DeclaringType);
public Box(string name) : base(name) { }
[ActionItem(ActionName = "Вытащить содержимое", ActionDescription = "Вытащить содержимое из коробки")]
public override object GetContent() => new object[] { "большие солнечные очки", "звонок от велосипеда", "вобла" };
}
class Room : IEnumerable {
List<Item> _items;
public Room() => _items = new List<Item>();
public void Add(Item i) => _items.Add(i);
public IEnumerator GetEnumerator() => _items.GetEnumerator();
}
internal class Program {
static void ConsoleWriteLine(object o) =>
Console.WriteLine(o is IEnumerable ? String.Join("; ", (object[])o) : o);
static void Main() {
var room = new Room { new WindowItem("Переднее окно"), new Cloth("Тряпка для доски"), new Box("Шкатулка") };
foreach ( Item itm in room) {
Console.WriteLine($"Предмет: {itm.Name}");
foreach (var method in itm.ActionableItems)
if (itm.GetType().GetMethod(method.MethodName).ReturnType != typeof(void))
ConsoleWriteLine(itm.Act(method.MethodName));
else itm.Act(method.MethodName);
}
}
}
-->
Предмет: Переднее окно
Открыть: Открыть окно
Закрыть: Закрыть окно
Взять: Ага, попробуй возьми.
Вытащить содержимое: Аааа, ломать не нужно!!!
Предмет: Тряпка для доски
Взять: Не забудь вернуть обратно.
Вытащить содержимое: Аааа, ломать не нужно!!!
Предмет: Шкатулка
Вытащить содержимое: большие солнечные очки; звонок от велосипеда; вобла
Взять: Не забудь вернуть обратно.
В классе Item есть общие методы Take и GetContent. В классе WindowItem есть уникальные методы Open и Close, а также доступен унаследованный метод GetContent и переопределённый метод Take. В классе Cloth нет уникальных методов, которые попадают в список, он использует унаследованные методы Take и GetContent.
От создания списков методов вручную я решил отказаться. Каждый метод, который можно запускать для класса, нужно пометить атрибутом ActionItemAttribute. Тогда список можно построить автоматически. Метод Cloth.Wash не помечен атрибутом ActionItemAttribute, поэтому он не попадает в список.
ActionItemAttribute несёт в себе дополнительную информацию, пример использования которой есть в методах Open и Close.
В классе Item есть свойство ActionableItems, которое выдаёт для класса список методов с ActionItemAttribute. Метод Act позволяет запускать метод по его имени.
UPD Переделал для ускорения работы. Поскольку список методов для класса зависит только от кода, перенёс генерацию списка в инициализацию статического поля класса. Свойство ActionableItems нестатическое для того, чтобы получить список сразу от объекта.
UPD Добавил возможность что-то возвращать. Для этого нужный метод должен возвращать значение, которое в Act боксится в объект. Добавил в код комментарии, что использовать и как.
UPD И, наконец, добавил класс Room с коллекцией Item внутри. Для демонстрационных целей выполняются все действия для каждого предмета. Естественно, можно выполнять только нужные. Дополнительно можно в ActionItemAttribute добавить классификацию действий и другие признаки.