Unity菜单系统抽象类设计验证:静态与动态菜单方案咨询
场景与疑问
我正在Unity中设计一套菜单系统,包含:
- 承载共享功能的抽象类
BaseMenu - 两种具体菜单类型:
StaticMenu:在Unity编辑器中预先指定按钮DynamicMenu:根据游戏上下文(如对话选项)在运行时动态创建按钮
我的疑问:
BaseMenu的设计能否满足StaticMenu和DynamicMenu的需求?- 当前设计是否存在违反SOLID原则的潜在问题?
- 这是否为正确方案,是否应考虑使用接口或其他设计模式?
代码实现
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

