08. 用接口设计模块(依赖倒置原则)

QFramework 本身支持依赖倒置原则,就是所有的模块访问和交互都可以通过接口来完成,代码如下:

using UnityEngine;
using UnityEngine.UI;

namespace QFramework.Example
{

    // 1. 定义一个 Model 对象
    public interface ICounterAppModel : IModel
    {
        BindableProperty<int> Count { get; }
    }
    public class CounterAppModel : AbstractModel,ICounterAppModel
    {
        public BindableProperty<int> Count { get; } = new BindableProperty<int>();

        protected override void OnInit()
        {
            var storage = this.GetUtility<IStorage>();

            // 设置初始值(不触发事件)
            Count.SetValueWithoutEvent(storage.LoadInt(nameof(Count)));

            // 当数据变更时 存储数据
            Count.Register(newCount =>
            {
                storage.SaveInt(nameof(Count),newCount);
            });
        }
    }

    public interface IAchievementSystem : ISystem
    {

    }

    public class AchievementSystem : AbstractSystem ,IAchievementSystem
    {
        protected override void OnInit()
        {
            this.GetModel<ICounterAppModel>() // -+
                .Count
                .Register(newCount =>
                {
                    if (newCount == 10)
                    {
                        Debug.Log("触发 点击达人 成就");
                    }
                    else if (newCount == 20)
                    {
                        Debug.Log("触发 点击专家 成就");
                    }
                    else if (newCount == -10)
                    {
                        Debug.Log("触发 点击菜鸟 成就");
                    }
                });
        }
    }

    public interface IStorage : IUtility
    {
        void SaveInt(string key, int value);
        int LoadInt(string key, int defaultValue = 0);
    }

    public class Storage : IStorage
    {
        public void SaveInt(string key, int value)
        {
            PlayerPrefs.SetInt(key,value);
        }

        public int LoadInt(string key, int defaultValue = 0)
        {
            return PlayerPrefs.GetInt(key, defaultValue);
        }
    }


    // 2.定义一个架构(提供 MVC、分层、模块管理等)
    public class CounterApp : Architecture<CounterApp>
    {
        protected override void Init()
        {
            // 注册 System 
            this.RegisterSystem<IAchievementSystem>(new AchievementSystem()); 

            // 注册 Model
            this.RegisterModel<ICounterAppModel>(new CounterAppModel());

            // 注册存储工具的对象
            this.RegisterUtility<IStorage>(new Storage());
        }
    }

    // 引入 Command
    public class IncreaseCountCommand : AbstractCommand 
    {
        protected override void OnExecute()
        {
            var model = this.GetModel<ICounterAppModel>();

            model.Count.Value++; // -+
        }
    }

    public class DecreaseCountCommand : AbstractCommand
    {
        protected override void OnExecute()
        {
            this.GetModel<ICounterAppModel>().Count.Value--; // -+
        }
    }

    // Controller
  public class CounterAppController : MonoBehaviour , IController /* 3.实现 IController 接口 */
    {
        // View
        private Button mBtnAdd;
        private Button mBtnSub;
        private Text mCountText;

        // 4. Model
        private ICounterAppModel mModel;

        void Start()
        {
            // 5. 获取模型
            mModel = this.GetModel<ICounterAppModel>();

            // View 组件获取
            mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();
            mBtnSub = transform.Find("BtnSub").GetComponent<Button>();
            mCountText = transform.Find("CountText").GetComponent<Text>();


            // 监听输入
            mBtnAdd.onClick.AddListener(() =>
            {
                // 交互逻辑this.SendCommand<IncreaseCountCommand>();
            });

            mBtnSub.onClick.AddListener(() =>
            {
                // 交互逻辑
                this.SendCommand(new DecreaseCountCommand(/* 这里可以传参(如果有) */));
            });

            // 表现逻辑
            mModel.Count.RegisterWithInitValue(newCount => // -+
            {
                UpdateView();

            }).UnRegisterWhenGameObjectDestroyed(gameObject);
        }

        void UpdateView()
        {
            mCountText.text = mModel.Count.ToString();
        }

        // 3.public IArchitecture GetArchitecture()
        {
            return CounterApp.Interface;
        }

        private void OnDestroy()
        {
            // 8. 将 Model 设置为空
            mModel = null;
        }
    }
}

代码不难。

所有的模块注册,模块获取 等代码都是通过接口完成,这一点符合 SOLID 原则中的 依赖倒置原则。

通过接口设计模块可以让我们更容易思考模块之间的交互和职责本身,而不是具体实现,在设计的时候可以减少很多的干扰。

当然面向接口的方式去做开发也有很多其他的好处,这当然是大家随着使用时长会慢慢体会的。

其中有一个重要的大一点,就是我们之前说的 Storage,如果想把存储的 API 从 PlayerPrefs 切换成 EasySave,那么我们就不需要去修改 Storage 对象,而是扩展一个 IStorage 接口即可,伪代码如下:

    public class EasySaveStorage : IStorage
    {
        public void SaveInt(string key, int value)
        {
            // todo
        }

        public int LoadInt(string key, int defaultValue = 0)
        {
            // todothrow new System.NotImplementedException();
        }
    }

注册模块的伪代码如下:

    // 定义一个架构(用于管理模块)
    public class CounterApp : Architecture<CounterApp>
    {
        protected override void Init()
        {
            // 注册成就系统
            this.RegisterSystem<IAchievementSystem>(new AchievementSystem());

            this.RegisterModel<ICounterAppModel>(new CounterAppModel());

            // 注册存储工具对象
            // this.RegisterUtility<IStorage>(new Storage());
            this.RegisterUtility<IStorage>(new EasySaveStorage());
        }
    }

这样,底层所有存储的代码都切换成了 EasySave 的存储,替换一套方案非常简单。

好了,用接口设计模块的功能就介绍完了。

这篇内容就这些。

总结笔记

  1. Dependency Inversion Principle

  2. 总的来说就是在各个组件中, 如果需要获取其他组件, 都是使用Architecture 提供的方法来获取到, 而这些方法都是基于接口的. 具体的类型信息都是通过泛型传递进去的, 比如

    1. CounterApp 注册组件, 使用 RegisterXXX

    2. CounterAppController 中调用 Command, 使用的是 SendCommand

    3. System 中获取 Model, 使用的是 GetModel

    4. Command 中获取 Model, 使用的 GetModel

    5. Model 中获取 Utility 使用的 GetUtility

附上一个类图, 方便理解 DIP 原则: