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

Рейтинг: 1Ответов: 3Опубликовано: 20.08.2023

Всем привет. Есть такая задача: есть класс, пусть будет Item. Он является родительским классом для всех предметов в комнате и имеет какие-то общие методы - Take(), CreateDust() и т.д. Также есть классы для разные предметов - Chair, TV, Box. Помимо того, что они могут иметь разные реализации родительских методов, они могут иметь и свои уникальные - допустим, метод GetContent() для класса Box и SetEnabled() для TV. Есть также класс Room, который хранит массив Item items[] и каждый тик проходится в цикле по этому массиву и совершает некоторые действия. В секунду может быть несколько десятков тиков и, соответственно, вызовов цикла.

И наконец, перейдём непосредственно к вопросу. Как реализовать выполнение методов дочерних классов, если храним мы только Item? У меня было несколько вариантов:

  1. Создать несколько массивов, где хранить именно дочерние классы (Chair, TV, Box), заполнять их при инициализации комнаты и проходиться в цикле по ним
  2. Использовать оператор is внутри общего цикла, получать нужный дочерний класс и работать с ним
  3. Создавать виртуальные методы (GetContent(), SetEnabled()) в родительском классе, переопределять их в дочерних и в общем цикле вызывать эти методы через родительский класс, но что-то делать они будут только если это элемент нужного класса

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

Есть ли какие-то другие, более оптимальные способы, или нужно доработать один из моих? Пишу на C#

Ответы

▲ 0Принят

Немного не понял, почему вам не нравится использование is, ведь нам всё равно придётся каким либо образом узнать, где какой класс, иначе не понятно, у кого именно вызывать этот метод, ведь он может отсутствовать у родительского класса

Допустим мы реализовали следующие классы:

public abstract class Item
{
    public virtual string GetName() => "Item";
}

public class CustomItem : Item
{
    string name;
    public CustomItem(string name) => this.name=name;
    public override string GetName() => name;
    public void SetName(string newName) => name=newName;
}

public class Box : Item
{
    public override string GetName() => "Box";
    public Item[] Content = new Item[0];
    public virtual Item[] GetContent() => Content;
}
public class TV : Item
{
    public override string GetName() => "TV";
    bool enable;
    public void SetEnable(bool enable) => this.enable = enable;
}

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

Например: Item item = new Box(); item.GetName() вернёт "Box"

Если же мы хотим получить доступ к методам дочерних объектов, то нам надо родительский класс преобразовать к этому дочернему объекту с помощью оператора is, as или явного преобразования (T converted = (T)original)

Вот как всё это примерно может выглядеть:

foreach (var item in items)
{
    Console.WriteLine($"Name: {item.GetName()}");
    if (item is TV) // Если item является типом TV
    {
        TV tv = (TV)item; // Преобразуем Item в TV
        tv.SetEnable(true); // Имеем доступ к методам
    } 
    else if (item is Box box) // Более удобный способ
    {
        Console.WriteLine(box.Content);
    }

    // Ещё один плохой способ:
    CustomItem? customItem = item as CustomItem;
    if (customItem != null)
    {
        customItem.SetName("");
    }
    // Но если надо просто вызвать один метод, то это хороший способ:
    (item as CustomItem)?.SetName("New Name");
}

P.S.: По хорошему public virtual string GetName() => "Item"; стоит заменить на public virtual string Name => "Item"; превратив его в переопределяемое Getter поле. Аналогично рекомендую использовать public virtual bool Enabled {get; set;} для TV и других включаемых предметов А ещё лучше, если это свойство будет вынесено в отдельный интерфейс interface ITrigger {public bool Enable {get; set;}, если объектов с подобным методом много

▲ 2

Вариант номер 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 добавить классификацию действий и другие признаки.

▲ 0

Оба три предложенных варианта - плохие с точки зрения ООП.

Самый простой вариант решения такой задачи в парадигме ООП:

  1. В базовом классе Item создается абстрактный метод Do

  2. В каждом потомке делается нужная реализация данного метода Do(Для коробки - извлечение содержимого, для телевизора - включить-выключить и тд)

  3. В вызывающем классе в цикле вызывается для каждого элемента в массиве вызывается метод Item.Do()

    public abstract class Item
    {
        public abstract void Do(Action _action);
    }
    
    public class CustomItem : Item
    {
        public override void Do(Action _action)
        { 
            if (_action.type() == "Что-то сделать")
            {
                // Что-то сделать
            }
        }
    }
    
    public class Box : Item
    {
        public override void Do(Action _action)
        {
            if (_action.type() == "Открыть коробку")
            {
                // Открыть коробку
            }                
        }
    }
    
    public class TV : Item
    {
        public override void Do(Action _action)
        {
            switch (_action.type())
            {
                case "Включить телевизор" :
                    // Включить телевизор
                    break;
                case "Выключить телевизор" :
                    // Выключить телевизор
                    break;
                case "Переключить программу" :
                    // Переключить программу
                    break;
            }                
        }
    }
    

в классе-обработчике: Action action = new Action("Открыть коробку"); foreach (Item item in items) { item.Do(action); }

Может возникнуть вопрос: Как отличить внутри Do какое конкретно действие необходимо выполнить для конкретного Item и с какими параметрами?

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

А уже каждый потомок Item внутри Do в зависимости от того, что он должен будет делать - будет приводить тип класса Action к нужному потомку и уже обрабатывать действие с параметрами. Например телевизор можно включить или выключить или переключить программу.