深入浅出PureMVC框架:从理论到Unity实战

13 阅读6分钟

一、PureMVC是什么?

一句话定义:PureMVC是一个轻量级的、基于经典MVC模式的应用程序框架,核心目标是将代码按职责分离,实现高内聚低耦合

PureMVC框架链接puremvc.org/ 找到对应的语言下载即可

二、为什么需要PureMVC?

没有框架的Unity项目长什么样?

csharp

// 典型“上帝类”写法 —— 所有逻辑堆在一个MonoBehaviour里
public class BattleManager : MonoBehaviour
{
    public UIManager ui;
    public NetworkManager network;
    public DataManager data;
    
    void OnEnemyDie()
    {
        ui.ShowVictoryPanel();
        data.AddExp(100);
        network.SendBattleResult();
        // 越往后越难维护...
    }
}

问题

  1. 代码耦合度高,改一个地方炸一片
  2. 新人不敢改老代码
  3. 单元测试几乎不可能写
  4. 多人协作时频繁冲突

三、PureMVC核心组件详解

1. Facade —— 统一入口(门面)

角色定位:整个框架的“前台总机”,所有对外操作都通过它。

(简单来说就是Mediator、Proxy、Command之间都不会互相调用,因为这样会非常的复杂,不便于维护,而是统一通过调用Facade里的类似GetProxy()、GetMediator()函数来间接获取目标引用,这样通过在Facade实现统一的接口来让外部调用的方法很好的解决了模块间直接引用导致的逻辑混乱)

csharp

// 标准用法:项目里写一个自己的Facade继承基类
public class GameFacade : Facade
{
    // 单例访问
    public static GameFacade Instance => instance as GameFacade;
    
    // 启动框架
    public void Startup()
    {
        // 注册Proxy
        RegisterProxy(new UserProxy());
        RegisterProxy(new BagProxy());
        
        // 注册Command(绑定事件与处理逻辑)
        RegisterCommand(NotificationName.LOGIN, () => new LoginCommand());
        RegisterCommand(NotificationName.BAG_ADD_ITEM, () => new AddItemCommand());
        
        // 注册Mediator(通常由UIManager在打开界面时动态注册)
    }
}

// 业务代码中调用
GameFacade.Instance.SendNotification(NotificationName.LOGIN, userData);

核心职责

  • 初始化Model、View、Controller三大核心模块
  • 注册/获取Proxy、Mediator、Command
  • 发送Notification

2. Proxy —— 数据与业务代理

角色定位:管理某一类数据,以及操作这些数据的方法。

csharp

// 数据代理:管理玩家背包
public class BagProxy : Proxy
{
    // Proxy名称(用于跨模块获取)
    public new const string NAME = "BagProxy";
    
    // 实际数据
    public List<Item> Items { get; private set; } = new List<Item>();
    
    // 业务方法
    public void AddItem(Item item)
    {
        Items.Add(item);
        // 数据变了,发通知告诉UI更新
        SendNotification(NotificationName.BAG_UPDATE, Items.Count);
    }
    
    public bool HasItem(int itemId)
    {
        return Items.Any(item => item.Id == itemId);
    }
    
    public void RemoveItem(int itemId)
    {
        Items.RemoveAll(item => item.Id == itemId);
        SendNotification(NotificationName.BAG_UPDATE);
    }
}

// 其他地方获取并使用
var bagProxy = GameFacade.Instance.RetrieveProxy(BagProxy.NAME) as BagProxy;
bagProxy.AddItem(new Item(10001, "红药水"));

关键理解

  • Proxy 只发Notification,不收Notification —— 这是PureMVC刻意设计,保证Model层独立,不依赖其他层
  • Proxy可以持有网络请求逻辑(但盛趣项目通常网络层单独封装,Proxy只负责调用)

3. Mediator —— 视图的中介

角色定位:UI界面和PureMVC系统之间的“翻译官”。

csharp

// 中介者:管理一个背包面板
public class BagMediator : Mediator
{
    public new const string NAME = "BagMediator";
    
    // 持有的UI组件引用
    private BagPanel bagPanel;
    
    // 构造函数:传入View组件
    public BagMediator(BagPanel panel) : base(NAME)
    {
        bagPanel = panel;
        bagPanel.OnItemClick += HandleItemClick; // 监听UI事件
    }
    
    // 声明感兴趣的通知(订阅)
    public override IList<string> ListNotificationInterests()
    {
        return new List<string>
        {
            NotificationName.BAG_UPDATE,
            NotificationName.ITEM_USE_RESULT
        };
    }
    
    // 处理通知
    public override void HandleNotification(INotification notification)
    {
        switch (notification.Name)
        {
            case NotificationName.BAG_UPDATE:
                UpdateBagView();
                break;
            case NotificationName.ITEM_USE_RESULT:
                ShowUseResult(notification.Body as string);
                break;
        }
    }
    
    // 视图更新逻辑
    private void UpdateBagView()
    {
        var bagProxy = Facade.RetrieveProxy(BagProxy.NAME) as BagProxy;
        bagPanel.RefreshItems(bagProxy.Items);
    }
    
    // UI事件响应
    private void HandleItemClick(Item item)
    {
        // Mediator不处理业务逻辑,发个通知交给Command
        SendNotification(NotificationName.USE_ITEM, item.Id);
    }
    
    // Mediator销毁时的清理
    public override void OnRemove()
    {
        bagPanel.OnItemClick -= HandleItemClick;
        base.OnRemove();
    }
}

关键理解

  • Mediator 既不持有数据(数据在Proxy),也不处理业务逻辑(逻辑在Command)
  • Mediator只做两件事:监听UI事件→发Notification出去 + 收到Notification→更新UI

4. Command —— 业务逻辑命令

角色定位:执行具体的业务操作,可以调用多个Proxy协同工作。

csharp

// 简单命令:使用物品
public class UseItemCommand : SimpleCommand
{
    public override void Execute(INotification notification)
    {
        int itemId = (int)notification.Body;
        
        var bagProxy = Facade.RetrieveProxy(BagProxy.NAME) as BagProxy;
        var roleProxy = Facade.RetrieveProxy(RoleProxy.NAME) as RoleProxy;
        
        if (bagProxy.HasItem(itemId))
        {
            bagProxy.RemoveItem(itemId);
            roleProxy.AddHp(100);
            
            // 发通知让UI刷新
            SendNotification(NotificationName.ROLE_HP_UPDATE);
            SendNotification(NotificationName.USE_ITEM_SUCCESS, itemId);
        }
        else
        {
            SendNotification(NotificationName.USE_ITEM_FAIL, "物品不存在");
        }
    }
}

// 宏命令:执行一系列命令(比如登录流程)
public class LoginMacroCommand : MacroCommand
{
    public override void InitializeMacroCommand()
    {
        AddSubCommand(() => new CheckVersionCommand());    // 1. 检查版本
        AddSubCommand(() => new ConnectServerCommand());   // 2. 连接服务器
        AddSubCommand(() => new AuthCommand());            // 3. 身份验证
        AddSubCommand(() => new LoadRoleDataCommand());    // 4. 加载角色数据
        AddSubCommand(() => new EnterGameCommand());       // 5. 进入游戏
    }
}

Command的特点

  • 无状态:每次执行都创建新实例(由框架管理)
  • 单一职责:一个Command只做一件事
  • 可组合:MacroCommand可以把多个SimpleCommand串起来

5. Notification —— 通信的载体

角色定位:模块间传递消息的信封。

csharp

// 定义通知名称常量(避免字符串硬编码)
public static class NotificationName
{
    public const string LOGIN = "login";
    public const string LOGOUT = "logout";
    public const string BAG_UPDATE = "bag_update";
    public const string USE_ITEM = "use_item";
    public const string USE_ITEM_SUCCESS = "use_item_success";
}

// 发送通知的三种重载
SendNotification(NotificationName.LOGIN);                        // 只有名称
SendNotification(NotificationName.LOGIN, loginData);             // 带body(数据)
SendNotification(NotificationName.LOGIN, loginData, "extra");    // 带body和type

Notification与C#事件的对比

特性C#事件PureMVC Notification
解耦程度中等(需要持有发布者引用)高(完全不知道谁在收)
调试难度容易跟踪难(字符串匹配)
性能快(直接委托调用)稍慢(反射+装箱拆箱)
跨模块通信需要统一的事件总线天然支持

四、完整流程示例:登录功能

把上面所有组件串起来,看一个完整的登录流程:

csharp

// 1. 启动框架(GameManager中)
GameFacade.Instance.Startup();

// 2. UI按钮点击 -> 打开登录面板时注册Mediator
LoginPanel panel = UIManager.Open<LoginPanel>();
GameFacade.Instance.RegisterMediator(new LoginMediator(panel));

// 3. 用户点击登录按钮 -> Mediator监听到UI事件
public class LoginMediator : Mediator
{
    private LoginPanel panel;
    
    public LoginMediator(LoginPanel panel) : base("LoginMediator")
    {
        this.panel = panel;
        panel.OnLoginClick += (user, pwd) => 
            SendNotification(NotificationName.LOGIN, new LoginData(user, pwd));
    }
}

// 4. Command处理登录业务
public class LoginCommand : SimpleCommand
{
    public override void Execute(INotification notification)
    {
        var loginData = notification.Body as LoginData;
        var userProxy = Facade.RetrieveProxy(UserProxy.NAME) as UserProxy;
        
        // 调用网络层发送登录请求
        NetworkManager.Instance.Login(loginData.User, loginData.Pwd, (success, msg) =>
        {
            if (success)
            {
                userProxy.SetUserInfo(msg);
                SendNotification(NotificationName.LOGIN_SUCCESS);
                SendNotification(NotificationName.OPEN_MAIN_PANEL);
            }
            else
            {
                SendNotification(NotificationName.LOGIN_FAIL, msg);
            }
        });
    }
}

// 5. 登录成功 -> 关闭登录界面,打开主界面
public class LoginSuccessCommand : SimpleCommand
{
    public override void Execute(INotification notification)
    {
        // 移除登录Mediator
        Facade.RemoveMediator("LoginMediator");
        // 打开主界面并注册其Mediator
        MainPanel mainPanel = UIManager.Open<MainPanel>();
        Facade.RegisterMediator(new MainMediator(mainPanel));
    }
}

五、PureMVC在Unity中的注意事项

1. MonoBehaviour与PureMVC的关系

原则:Mediator持有MonoBehaviour的引用,但Mediator本身不继承MonoBehaviour。

csharp

// 错误:让Mediator继承MonoBehaviour
public class BadMediator : MonoBehaviour, IMediator { } // ❌

// 正确:Mediator是纯C#类
public class GoodMediator : Mediator  // 不继承MonoBehaviour ✓
{
    private GoodPanel panel; // 持有MonoBehaviour引用
}

2. 生命周期管理

组件创建时机销毁时机
Facade游戏启动时游戏结束时
Proxy游戏启动时注册游戏结束时
Command每次执行时new执行完后销毁
Mediator打开UI时注册关闭UI时移除

3. 性能优化建议

  • 避免频繁发Notification:一帧内多次发同一个通知可以合并
  • Mediator的ListNotificationInterests返回缓存列表,不要每次new
  • Command尽量轻量:耗时操作要用协程或异步

六、PureMVC的优缺点总结

优点

  1. 约定大于配置:规定好了代码该放哪,团队协作不吵架
  2. 完全解耦:改一个模块基本不影响其他模块
  3. 可测试性强:Proxy和Command可以脱离Unity写单元测试
  4. 学习成本低:核心概念就5个,一两天就能上手

缺点

  1. 代码冗余:一个小功能也要建Mediator+Command+Proxy三个类
  2. Notification难追踪:字符串事件名,谁发谁收不直观
  3. 反射开销:框架内部大量使用反射,但影响不大
  4. 不够Unity原生:不支持MonoBehaviour的生命周期方法