You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity菜单系统抽象类设计验证:静态与动态菜单方案咨询

Unity菜单系统SOLID原则验证(重点:里氏替换原则)

场景与疑问

我正在Unity中设计一套菜单系统,包含:

  • 承载共享功能的抽象类BaseMenu
  • 两种具体菜单类型:
    • StaticMenu:在Unity编辑器中预先指定按钮
    • DynamicMenu:根据游戏上下文(如对话选项)在运行时动态创建按钮

我的疑问:

  1. BaseMenu的设计能否满足StaticMenu和DynamicMenu的需求?
  2. 当前设计是否存在违反SOLID原则的潜在问题?
  3. 这是否为正确方案,是否应考虑使用接口或其他设计模式?

代码实现

BaseMenu抽象类

public abstract class BaseMenu
{
    public bool IsActive { get; set; }
    public RectTransform Panel { get; protected set; }
    public List<Button> Buttons { get; protected set; }
    public int CurrentIndex { get; protected set; }

    public virtual void Initialize()
    {
        CurrentIndex = 0;
        Buttons = new List<Button>(); // Ensures Buttons is always initialized
        SetupButtons(); // Abstract method to allow flexibility
    }

    public virtual void Show()
    {
        Panel.gameObject.SetActive(true);
        IsActive = true;
        RegisterWithMenuManager();
    }

    public virtual void Hide()
    {
        Panel.gameObject.SetActive(false);
        IsActive = false;
        DeregisterWithMenuManager();
    }

    public virtual void SelectNext()
    {
        CurrentIndex = (CurrentIndex + 1) % Buttons.Count;
        // Add visual selection logic here
    }

    public virtual void SelectPrevious()
    {
        CurrentIndex = (CurrentIndex - 1 + Buttons.Count) % Buttons.Count;
        // Add visual selection logic here
    }

    public abstract void HandleInput(); // Subclass defines input logic (e.g., vertical vs horizontal)
    protected abstract void SetupButtons(); // Subclass defines how buttons are set up

    public void RegisterWithMenuManager()
    {
        MenuManager.Instance.Register(this); // Add this menu to a stack of active menus
    }

    public void DeregisterWithMenuManager()
    {
        MenuManager.Instance.Deregister(this); // Remove this menu from the stack
    }
}

StaticMenu实现(编辑器预定义按钮)

public class StaticMenu : BaseMenu
{
    [SerializeField] private List<Button> serializedButtons;

    protected override void SetupButtons()
    {
        Buttons = serializedButtons; // Use predefined buttons
    }

    public override void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.Up))
        {
            SelectPrevious();
        }
        else if (Input.GetKeyDown(KeyCode.Down))
        {
            SelectNext();
        }
    }
}

DynamicMenu实现(运行时动态生成按钮)

public class DynamicMenu : BaseMenu
{
    private List<string> options;

    public DynamicMenu(List<string> options)
    {
        this.options = options;
    }

    protected override void SetupButtons()
    {
        foreach (var option in options)
        {
            Button newButton = InstantiateButton(option);
            Buttons.Add(newButton);
        }
    }

    private Button InstantiateButton(string text)
    {
        GameObject buttonPrefab = Resources.Load<GameObject>("ButtonPrefab");
        Button button = Instantiate(buttonPrefab, Panel).GetComponent<Button>();
        button.GetComponentInChildren<Text>().text = text;
        return button;
    }

    public override void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.Up))
        {
            SelectPrevious();
        }
        else if (Input.GetKeyDown(KeyCode.Down))
        {
            SelectNext();
        }
    }
}

问题解答

1. BaseMenu设计能否满足需求?

基本能覆盖核心需求。BaseMenu封装了菜单的通用逻辑(显示/隐藏、管理器注册、上下项选择),通过抽象方法SetupButtons和HandleInput让子类实现差异化逻辑,不管是编辑器预定义按钮的StaticMenu,还是运行时动态生成按钮的DynamicMenu,都能通过重写抽象方法适配场景。

2. 当前设计的SOLID潜在问题

里氏替换原则(LSP)风险

  • DynamicMenu使用带参数的构造函数,但从StaticMenu的SerializeField可以推断,菜单类应该是Unity的MonoBehaviour子类。Unity组件不能通过new实例化,必须用AddComponent,这会导致DynamicMenu的参数构造函数无法被正确调用,替换BaseMenu实例时会出现options为空的错误,违反了LSP“子类可无缝替换父类”的要求。
  • BaseMenu的Initialize方法先初始化Buttons = new List<Button>(),但StaticMenu直接将Buttons赋值为serializedButtons,如果后续基类逻辑依赖自身创建的List实例(比如添加集合监听、批量修改),子类的赋值操作会破坏基类的预期行为。

依赖倒置原则(DIP)违反

BaseMenu直接依赖MenuManager.Instance静态单例,紧耦合了具体实现。后续如果要替换MenuManager的逻辑(比如测试用的模拟管理器),必须修改BaseMenu代码,不符合“依赖抽象而非具体实现”的原则。

单一职责原则(SRP)模糊

BaseMenu同时承担了菜单状态管理(IsActive、CurrentIndex)、UI显示控制、管理器注册、输入逻辑抽象等多个职责,后续扩展时容易变得臃肿,难以维护。

3. 优化方案与设计模式建议

当前方案有改进空间,推荐以下调整:

引入接口定义契约

定义IMenu接口明确菜单核心行为,让BaseMenu实现该接口,降低耦合度:

public interface IMenu
{
    bool IsActive { get; }
    void Initialize();
    void Show();
    void Hide();
    void HandleInput();
}

BaseMenu作为抽象类实现IMenu并封装通用逻辑,后续如果有不继承BaseMenu的菜单实现,也能遵循同一契约。

修正Unity组件实例化问题

DynamicMenu去掉带参数的构造函数,改为提供SetOptions方法(Unity组件必须通过AddComponent创建):

public class DynamicMenu : BaseMenu
{
    private List<string> options;

    public void SetOptions(List<string> options)
    {
        this.options = options;
    }

    // 其他逻辑不变
}

使用示例:

var dynamicMenu = panel.AddComponent<DynamicMenu>();
dynamicMenu.SetOptions(dialogOptions);
dynamicMenu.Initialize();

抽离按钮创建逻辑(工厂模式)

把动态创建按钮的逻辑从DynamicMenu中抽离,用ButtonFactory类处理,降低DynamicMenu的职责,符合SRP:

public static class ButtonFactory
{
    public static Button CreateButton(string text, RectTransform parent)
    {
        GameObject buttonPrefab = Resources.Load<GameObject>("ButtonPrefab");
        Button button = Instantiate(buttonPrefab, parent).GetComponent<Button>();
        button.GetComponentInChildren<Text>().text = text;
        return button;
    }
}

DynamicMenu中调用方式:

protected override void SetupButtons()
{
    foreach (var option in options)
    {
        Button newButton = ButtonFactory.CreateButton(option, Panel);
        Buttons.Add(newButton);
    }
}

解耦MenuManager依赖

让BaseMenu依赖IMenuManager接口,而非具体单例实现,通过构造函数或属性注入:

public interface IMenuManager
{
    void Register(IMenu menu);
    void Deregister(IMenu menu);
}

public abstract class BaseMenu : IMenu
{
    protected readonly IMenuManager _menuManager;

    // 构造函数注入(如果是MonoBehaviour,可通过Awake时获取或依赖注入框架实现)
    public BaseMenu(IMenuManager menuManager)
    {
        _menuManager = menuManager;
    }

    public void RegisterWithMenuManager()
    {
        _menuManager.Register(this);
    }

    // 其他逻辑不变
}

内容的提问来源于stack exchange,提问作者xxSithRagexx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:24:54