Qframework框架学习
一、用接口设计模块(依赖倒置原则)
QFramework本身支持依赖倒置原则,就是所有的模块访问和交互都可通过接口来完成
依赖倒置原则是面向对象设计中的一个重要的原则,他提倡高层模块不应该依赖于底层模块,二者都应该依赖于抽象(接口或抽象类)
依赖倒置原则:强调了高层策略性的代码应该与实现细节分离,通过定义接口或抽象类作为两者之间的桥梁,使得高层模块不直接依赖于低层模块的具体实现,而是依赖于接口。
接口:在 QFramework 中,接口用来定义行为和功能的契约,但不提供具体的实现。通过接口,不同的模块可以在不知道对方具体实现的情况下进行协作。
以下是代码示例:制作简易计数器用接口设计模块
using UnityEngine;
namespace QFramework.Example
{
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 value) {
return PlayerPrefs.GetInt(key, value);
}
}
}
using UnityEngine;
namespace QFramework.Example
{
public interface IAchievementSystem : ISystem
{
}
public class AchievementSystem:AbstractSystem, IAchievementSystem
{
protected override void OnInit()
{
var model = this.GetModel<ICounterModel>();
model.Count.Register(count =>
{
if (count == 10)
{
Debug.Log("点击达人成就达成");
}
else if (count == 20)
{
Debug.Log("点击专家成就达成");
}
else if (count == -10)
{
Debug.Log("点击菜鸟成就达成");
}
});
}
}
}
namespace QFramework.Example
{
//顶一个架构(提供MVC、分层、模块管理等)
public class CounterApp : Architecture<CounterApp>
{
protected override void Init()
{
//注册System
this.RegisterSystem<IAchievementSystem>(new AchievementSystem());
//注册Utility存储工具的对象
this.RegisterUtility<IStorage>(new Storage());
//注册Model
this.RegisterModel<ICounterModel>(new CounterModel());
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace QFramework.Example
{
//Controller
public class CounterAppController : MonoBehaviour,IController
{
//view
public Button BtnAdd;
public Button BtnSub;
public Text CounterText;
//Model
private ICounterModel mModel;
private void Start()
{
//获取模型
mModel = this.GetModel < ICounterModel > ();
BtnAdd.onClick.AddListener(() =>
{
//交互逻辑
this.SendCommand<IncreaseCountCommand>();
});
BtnSub.onClick.AddListener(() => {
//交互逻辑
this.SendCommand<DecreaseCountCommand>();
});
//表现逻辑
mModel.Count.RegisterWithInitValue(count =>
{
updateview();
}).UnRegisterWhenGameObjectDestroyed(gameObject);
}
void updateview()
{
CounterText.text = mModel.Count.ToString();
}
public IArchitecture GetArchitecture()
{
return CounterApp.Interface;
}
}
}
using UnityEngine;
namespace QFramework.Example
{
//定义一个Model对象
public interface ICounterModel : IModel
{
BindableProperty<int> Count { get; }
}
public class CounterModel : AbstractModel, ICounterModel
{
public BindableProperty<int> Count { get; } = new BindableProperty<int>(0);
protected override void OnInit()
{
var storage = this.GetUtility<IStorage>();
//设置初始值(不触发事件)
Count.Value = storage.LoadInt(nameof(Count),0);
//当数据变更时存储数据
Count.Register(count =>
{
storage.SaveInt(nameof(Count), count);
});
}
}
}
namespace QFramework.Example
{
//引入Command
public class DecreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
this.GetModel<ICounterModel>().Count.Value--;
}
}
}
using UnityEngine;
namespace QFramework.Example
{
//引入Command
public class IncreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
var couterModel = this.GetModel <ICounterModel> ();
couterModel.Count.Value++;
}
}
}
通过接口设计模块可以让我们更容易思考模块之间的交互和职责本身,而不是具体的实现,在设计的时候可以减少很多的干扰
作用
- 提高模块间的解耦性:通过接口定义模块间的交互,减少了模块之间的直接依赖,便于模块的独立开发、测试和维护。
- 增强灵活性和可扩展性:当需要更改或扩展系统功能时,只需替换实现了特定接口的类,而不需要修改依赖该接口的高层模块。
- 促进单元测试:由于高层模块依赖于接口而非具体的实现,因此可以轻松地为高层模块编写单元测试,并使用模拟对象(Mock)来替代真实的低层模块。
- 支持插件化架构:基于接口的设计允许第三方开发者创建符合接口规范的插件,无缝集成到现有系统中。
应用场景
- 游戏逻辑模块化:例如,在处理玩家控制、AI行为、物理计算等不同方面时,可以通过接口将这些逻辑分离开来,使得每个部分都可以独立发展和优化。
- 资源管理:通过定义资源加载器接口,可以让游戏在不同的运行环境下(如编辑器环境与发布环境)采用不同的资源加载策略,而无需改变依赖资源加载器的高层逻辑。
- 用户界面(UI):UI组件通常需要根据业务逻辑的变化做出响应,通过接口定义UI与后台逻辑的交互方式,可以使UI层与逻辑层保持松散耦合,方便UI的更新和重构。
- 网络通信:在网络游戏中,客户端和服务端之间的通信协议可以通过接口定义,这样即使底层通信技术发生变化(如从HTTP切换到WebSocket),也不影响上层应用逻辑。
二、Query介绍
Query 是CQRS中的Q,也就是Command Query Responsibility Saperation 中的 Query。
Query 是和Command对应的查询对象
在 CQRS 架构下,Query
是用于处理数据读取请求的部分,其主要职责是从数据源获取信息,并将其返回给调用者。与命令不同,查询不会对系统状态产生任何变更
Controller 中的表现逻辑更多是接收到数据变更事件之后,对 Model 或者 System 进行查询,而查询的时候,有的时候需要组合查询,比如多个 Model 一起查询,查询的数据可能还需要转换一下,这种查询的代码量比较多。尤其是像模拟经营或者非常重数据的项目,所以 QFramework 支持通过 Query 这样的一个概念,来解决这部分问题。
主要特性
- 只读操作:
Query
对象仅执行读取操作,不进行任何会改变系统状态的操作。 - 单一职责:每个
Query
应该有明确且单一的目的,即为了获取特定的数据集或单个实体的信息。 - 响应性:由于查询通常涉及数据检索,优化查询以提高响应速度和效率是非常重要的。
- 隔离性:通过使用 CQRS,查询逻辑可以从命令逻辑中分离出来,这有助于简化代码结构,增强可维护性和扩展性。
应用场景
- 复杂的数据视图:当需要构建复杂的数据显示视图时,可以利用查询来封装数据获取逻辑,简化控制器层的工作。
- 报告生成:生成各种业务报告时,可以通过查询快速地从多个数据源中提取所需信息。
- 实时数据监控:对于需要实时显示最新数据的应用程序,如游戏中的排行榜或者在线用户统计等,查询可以提供最新的数据快照。
架构规范与推荐用法
QFramework架构提供了四个层级:
- 表现层:IController
- 系统层:ISystem
- 数据层:IModel
- 工具层:IUtility
除了四个层级,还提供了Command、Query、Event、BindableProperty等概念和工具
层级规则
- 表现层:ViewController层。IController接口,负责接收输入和状态变化的表现,一般情况下,MonoBehaviour均为表现层
- 可以获取System、Model
- 可以发送Command、Query
- 可以监听Event
Controller的接口定义:
public interface IController : IBelongToArchitecture, ICanSendCommand, ICanGetSystem, ICanGetModel,ICanRegisterEvent, ICanSendQuery
{
}
#endregion
- 系统层:System层。ISystem接口,帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等
- 可以获取System、Model
- 可以监听Event
- 可以发送Event
System的接口定义:
public interface ISystem : IBelongToArchitecture, ICanSetArchitecture, ICanGetModel, ICanGetUtility,ICanRegisterEvent, ICanSendEvent, ICanGetSystem
{
void Init();
}
-
数据层:Model层。IModel接口,负责数据的定义、数据的增删查改方法的提供
-
可以获取 Utility
-
可以发送 Event
-
Model 的接口定义如下:
public interface IModel : IBelongToArchitecture, ICanSetArchitecture, ICanGetUtility, ICanSendEvent
{
void Init();
}
-
工具层:Utility层。IUtility接口,负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等。啥都干不了,可以集成第三方库,或者封装API
Utility 的接口定义如下:
#region Utility
public interface IUtility
{
}
#endregion
-
Command:命令,负责数据的增删改。
-
可以获取 System、Model
-
可以发送 Event、Command
-
Command 的接口定义如下:
public interface ICommand : IBelongToArchitecture, ICanSetArchitecture, ICanGetSystem, ICanGetModel, ICanGetUtility,ICanSendEvent, ICanSendCommand, ICanSendQuery
{
void Execute();
}
-
Query:查询、负责数据的查询
-
可以获取 System、Model
-
可以发送 Query
-
public interface IQuery<TResult> : IBelongToArchitecture, ICanSetArchitecture, ICanGetModel, ICanGetSystem,ICanSendQuery
{
TResult Do();
}
-
通用规则:
-
IController 更改 ISystem、IModel 的状态必须用Command
-
ISystem、IModel 状态发生变更后通知 IController 必须用事件或BindableProperty
-
IController可以获取ISystem、IModel对象来进行数据查询
-
ICommand、IQuery 不能有状态,
-
上层可以直接获取下层,下层不能获取上层对象
-
下层向上层通信用事件
-
上层向下层通信用方法调用(只是做查询,状态变更用 Command),IController 的交互逻辑为特别情况,只能用 Command
-
通用规则是理想状态下的一套规则,但是落实的实际项目,很有可能需要对以上规则做一些修改。
EditorCounterApp和给主程看的开发模式
在简易计数器的Counter的基础上,我们要实现一个编辑器版本的CounterApp,以下是效果图:
using UnityEditor;
using UnityEngine;
namespace QFramework.Example
{
public class EditorCounterApp : EditorWindow,IController
{
[MenuItem("CounterApp/Window")]
public static void Open()
{
var counterApp = GetWindow<EditorCounterApp>();
counterApp.Show();
}
ICounterModel mModel;
public IArchitecture GetArchitecture()
{
return CounterApp.Interface;
}
private void OnEnable()
{
mModel = this.GetModel<ICounterModel>();
}
private void OnDisable()
{
mModel = null;
}
private void OnGUI()
{
if (GUILayout.Button("+"))
{
this.SendCommand<IncreaseCountCommand>();
}
GUILayout.Label(mModel.Count.Value.ToString());
if (GUILayout.Button("-"))
{
this.SendCommand<DecreaseCountCommand>();
}
}
}
}
通过以上代码,我们就可以快速的实现CounterApp的编辑器版本,因QFramework写的APP,底层三层是可以复用的
底层的三层与表现层的通信方式有Command、回调/事件、方法/Query
我们可以把表现成类比为网页前端,而底层三层类比成服务器,那么Command、回调/事件、方法/Query其实就是类似于Http或者TCP的接口或协议,而接口或者协议只要做好约定,那么前端就不需要关心服务端的具体实现了,而服务器也不需要关心前端的具体实现,这样就组哟到了在分工时,将表现层和底层三层的工作分别给不同的人来负责
特性
- 界面设计:展示如何使用 Unity 的 UI 系统(如 UnityEngine.UI 组件)来构建用户界面。
- 数据绑定:通过 QFramework 提供的数据绑定机制,实现 UI 与后台逻辑之间的同步更新。
- 事件处理:示范如何使用事件系统来响应用户输入或其他游戏内事件。
- 资源管理:可能包括如何加载、卸载资源以及管理资源生命周期的例子。
- 持久化数据:有时会涉及如何保存和读取用户设置或应用状态的方法。