Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

QFramework

一、框架目的

  1. 难维护

    由于拖拽的时候,对象之间的引用关系是随机的没有规律可循的,所以当时间久了或者比 人来接手次项目时要理清其中的逻辑所花费的时间会很多。

  2. 团队难以协作

    很多的拖拽操作和逻辑是在场景中操作的,并且也会记录到场景里,所以在使用 git 或者 svn 或者官方的 plastics 的时候比较难以管理,容易造成冲突。

  3. 可扩展性差

    这个原因和第一条一样,拖拽的时候,对象之间的引用关系是随机的没有规律可循的,这 样会造成大量的耦合(牵一发动全身),后边当扩展功能的时候,花的时间越来越久。 而接下来的架构演化的方向就是针对以上的这些缺点针对性地进行解决。

而解决这三个的问题的途径-------低耦合、高内聚。

什么是耦合、内聚?

耦合就是对象、类的双向引用、循环引用;内聚就是同样类型的代码放 在一起。

低耦合:使对象之间的交互合理

• 方法调用,例如:A 调用 B 的 SayHello 方法(上下级)

• 委托或者回调,例如:Action

• 消息或事件,例如:TinyMessage

高内聚:使软件模块化

• 单例,例如:GameController、UIManager、ResourceManager

• IOC,例如:Extenject、uFrame 的 Container、StrangeIOC 的绑定等等

• 分层,例如:MVC、三层架构、领域驱动分层等等

二、设计原则

SOLID 设计模式的六大原则有:

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Law of Demeter:迪米特法则
  • Interface Segregation Principle:接口隔离原则
  • Dependence Inversion Principle:依赖倒置原则

把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。下面我们来分别看一下这六大设计原则。

1. 单一职责原则(Single Responsibility Principle)

一个类应该只有一个发生变化的原因

T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。也就是说职责P1和P2被耦合在了一起。

 public void ParseDefaultResource(JsonData data)
    {
        for (int i = 0; i < data.Count; i++)
        {
            AvatarInfo info = new AvatarInfo();
            info.aid = (string)data[i]["aid"];
            info.name = (string)data[i]["name"];
            info.isDefault = (bool)data[i]["is_default"];
            info.addr = (string)data[i]["addr"];
            info.hash = (string)data[i]["hash"];
            avatarResourceDic[info.aid] = info;
        }

        GameController.manager.StartCoroutine(DownloadAllAvatars());
    }
// 做了两件事:解析数据和开启下载;
// 应该使用回调

2. 开闭原则(Open Closed Principle)

一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭

    public async UniTask<bool> LoadRemoteResource() {
        if (resHasUpdate) {
            Debug.Log("Try to Load res From Web");
            res.Unload(true);
            res = null;
            downingSize = SizeRes;
            Report(0f);
            CachedAssetBundle ab = new CachedAssetBundle("res", Hash128.Parse(hashRes));
            var _req = UnityWebRequestAssetBundle.GetAssetBundle(urlRes, ab, CRCRes);
            var response = await _req.SendWebRequest().ToUniTask(progress: this);
        }
        if (equip == null) {
            Debug.Log("Try to Load equip From Web");
            downingSize = SizeEquip;
            Report(0f);
            CachedAssetBundle ab = new CachedAssetBundle("equip", Hash128.Parse(hashEquip));
            var _req = UnityWebRequestAssetBundle.GetAssetBundle(urlEquip, ab, CRCEquip);
            var response = await _req.SendWebRequest().ToUniTask(progress: this);
        }

        if (medal == null) {
            Debug.Log("Try to Load Medal From Web");
            downingSize = SizeMedal;
            Report(0f);
            CachedAssetBundle ab = new CachedAssetBundle("medal", Hash128.Parse(hashMedal));
            var _req = UnityWebRequestAssetBundle.GetAssetBundle(urlMedal, ab, CRCMedal);
        }
        return true;
    }
// 修改成本太高,代码重复度太高
// 应该以类型为参数传入

3. 里氏替换原则(Liskov Substitution Principle)

所有引用基类的地方必须能透明地使用其子类的对象

C#不支持多继承

public String getFirst(List<String> values) {
    return values.get(0);
}

// 对于getFirst方法,接受一个List接口类型的参数,那既可以传递一个ArrayList类型的参数:
List<String> values = new ArrayList<>();
values.add("a");
values.add("b");
String firstValue = getFirst(values);

// 又可以接收一个LinkedList参数:
List<String> values = new LinkedList<>();
values.add("a");
values.add("b");
String firstValue = getFirst(values);

// 错误示例,并没有返回值CustomList
class CustomList<T> extends ArrayList<T> {
    @Override
    public T get(int index) {
        throw new UnsupportedOperationException();
    }
}

4. 迪米特法则(Law of Demeter)

只与你的直接朋友交谈,不跟“陌生人”说话

其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

TinyMessage 已经改善现状

5. 接口隔离原则(Interface Segregation Principle)

1、客户端不应该依赖它不需要的接口。 2、类间的依赖关系应该建立在最小的接口上。

注:该原则中的接口,是一个泛泛而言的接口,不仅仅指Java中的接口,还包括其中的抽象类。

public class EquipBtns : MonoBehaviour, SettingInterface {
    public bool IsAnythingChanged() {
        return false;
    }
    
    public bool IsCorrectInput() {
        return true;
    }
    
    public void RecoveryData() {
        // 不需要的模块,无需重置数据
    }
    
    public void SaveSettings(Action succeed = null) {
        
    }
}
// Equip中添加了肤色页面,不得不继承接口SettingInterface
// 再好的架构,也很难满足UI的快速变化

6. 依赖倒置原则(Dependence Inversion Principle)

1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。 2、抽象不应该依赖于细节,细节应该依赖于抽象。

public class AppearanceUI : MonoBehaviour, JsonDataInterface, SettingInterface
{// 依赖于接口 JsonDataInterface SettingInterface
 // 肤色页面在创建人物和设置中功能相同,都需要IsInfoCompleted、SaveSettings等功能,
 // 但是在相同功能下的表现可能不相同,例如保存成功后的弹窗
 // 依赖于接口方便以后的扩展,例如设置页面也要添加新的功能设置默认数据,
 // 只需要在SettingInterface接口中添加SetDefaultData(),然后肤色等模块实现就可以,
 // 这样使得设置的所有子页面对外具备相同的表现,提高了代码的规范,同时有利于对代码进行维护
 // 从而父级不需要关心具体实现
    public enum AppearanceUseSceneType {
        CreatePlayer = 0,
        Setting
    }
    public AppearanceUseSceneType sceneType;

    private void Awake() {
        if (sceneType == AppearanceUseSceneType.Setting) {
                
        }

        if (sceneType == AppearanceUseSceneType.CreatePlayer) {
            
        }        
    }


    /******** JsonDataInterface  createPlayer使用 ******/
    public void GenerateData(JsonWriter w) {
        w.WritePropertyName("skin");
        w.Write(colorItemManager.selectItem.index);
    }

    public bool IsInfoCompleted() {
        return true;
    }

    public bool IsInfoCorrect() {
        return true;
    }

    /******** SettingInterface   ******/
    public void SaveSettings(Action success = null) {
        NetworkController.manager.SendAttrInfo(bytes, () => {
            colorItemManager.SetSelectItem(colorItemManager.selectItem);
            success?.Invoke();
        });
    }

    public bool IsAnythingChanged() {       
        return GameController.manager.userInfo.skin != (uint)colorItemManager.selectItem.index;
    }

    public bool IsCorrectInput() {
        return true;
    }

    public void RecoveryData() {
       colorItemManager.selectItem = colorItemList[(int)GameController.manager.userInfo.skin];
        InitUI();
    }
}

三、总体架构

架构分层

img

QFramework系统设计架构分为四层及其规则:

  • 表现层:ViewController层。IController接口,负责接收输入和状态变化时的表现,一般情况下,MonoBehaviour 均为表现层

    • 可以获取System
    • 可以获取Model
    • 可以发送Command
    • 可以监听Event
  • 系统层:System层。ISystem接口,帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等

    • 可以获取System
    • 可以获取Model
    • 可以监听Event
    • 可以发送Event
  • 数据层:Model层。IModel接口,负责数据的定义、数据的增删查改方法的提供

    • 可以获取Utility
    • 可以发送Event
  • 工具层:Utility层。IUtility接口,负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等。啥都干不了,可以集成第三方库,或者封装API

  • 除了四个层级,还有一个核心概念——Command

    • 可以获取System
    • 可以获取Model
    • 可以发送Event
    • 可以发送Command
  • 层级规则:

    • IController 更改 ISystem、IModel 的状态必须用Command
    • ISystem、IModel状态发生变更后通知IController必须用事件或BindableProperty
    • IController可以获取ISystem、IModel对象来进行数据查询
    • ICommand不能有状态
    • 上层可以直接获取下层,下层不能获取上层对象
    • 下层向上层通信用事件
    • 上层向下层通信用方法调用(只是做查询,状态变更用Command),IController的交互逻辑为特别情况,只能用Command

四、示例

示例效果

代码地址

模块注册

using QFramework;

namespace CounterApp
{
    public class CounterApp : Architecture<CounterApp>
    {
        protected override void Init()
        {
            RegisterSystem<IAchievementSystem>(new AchievementSystem());
            RegisterModel<ICounterModel>(new CounterModel());
            RegisterUtility<IStorage>(new PlayerPrefsStorage());
        }
    }
}

ViewController

using System;
using QFramework;
using UnityEngine;
using UnityEngine.UI;

namespace CounterApp
{
    public class CounterViewController : MonoBehaviour,IController
    {
        private ICounterModel mCounterModel;

        private void Start()
        {
            mCounterModel = this.GetModel<ICounterModel>();

            // 监听
            mCounterModel.Count.RegisterWithInitValue(newCount =>
            {
                transform.Find("CountText").GetComponent<Text>().text = newCount.ToString();
            }).UnRegisterWhenGameObjectDestroyed(gameObject);
            

            transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑
                this.SendCommand<AddCountCommand>();
            });

            transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑
                this.SendCommand<SubCountCommand>();
            });
        
        }
        
        private void OnDestroy()
        {

            mCounterModel = null;
        }

        IArchitecture IBelongToArchitecture.GetArchitecture()
        {
            return CounterApp.Interface;
        }
    }


    public interface ICounterModel : IModel
    {
        BindableProperty<int> Count { get; }
    }

    public class CounterModel : AbstractModel, ICounterModel
    {
        protected override void OnInit()
        {
            var storage = this.GetUtility<IStorage>();

            Count.Value = storage.LoadInt("COUNTER_COUNT", 0);

            Count.Register(count => { storage.SaveInt("COUNTER_COUNT", count); });
        }

        public BindableProperty<int> Count { get; } = new BindableProperty<int>()
        {
            Value = 0
        };
    }
}

System

using QFramework;
using UnityEngine;

namespace CounterApp
{
    public interface IAchievementSystem : ISystem
    {
        
    }

    public class AchievementSystem : AbstractSystem, IAchievementSystem
    {
        protected override void OnInit()
        {
            var counterModel = this.GetModel<ICounterModel>();

            var previousCount = counterModel.Count.Value;

            counterModel.Count.Register(newCount =>
            {

                if (previousCount < 10 && newCount >= 10)
                {
                    Debug.Log("解锁点击 10 次成就");
                }
                else if (previousCount < 20 && newCount >= 20)
                {
                    Debug.Log("解锁点击 20 次成就");
                }

                previousCount = newCount;
            });
        }
    }
}

Utility

#if UNITY_EDITOR
using UnityEditor;
#endif
using QFramework;
using UnityEngine;

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

    public class PlayerPrefsStorage : 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);
        }
    }

    public class EditorPrefsStorage : IStorage
    {
        public void SaveInt(string key, int value)
        {
#if UNITY_EDITOR
            EditorPrefs.SetInt(key, value);
#endif
        }

        public int LoadInt(string key, int defaultValue = 0)
        {
#if UNITY_EDITOR
            return EditorPrefs.GetInt(key, defaultValue);
#else
            return 0;
#endif
        }
    }
}

Command

namespace CounterApp {
    public class AddCountCommand : AbstractCommand {
        protected override void OnExecute() {
            this.GetModel<ICounterModel>().Count.Value++;
        }
    }

    public class SubCountCommand : AbstractCommand {
        protected override void OnExecute() {
            this.GetModel<ICounterModel>().Count.Value--;
        }
    }
}

五、实现细节

  1. BindableProperty 响应式数据
public static class CounterModel
 {
     private static int mCount = 0;
     public static Action<int> OnCountChanged ;
 
     public static int Count
     {
         get => mCount;
         set
         {
             if (value != mCount)
             {
                 mCount = value;
                 OnCountChanged?.Invoke(value);
             }
         }
     }
 }
  1. 交互逻辑:View -> Model ; 表现逻辑: Model -> View

img

  1. IOC 容器
using System;
using System.Collections.Generic;

namespace FrameworkDesign
{
    public class IOCContainer
    {
        /// <summary>
        /// 实例
        /// </summary>
        public Dictionary<Type, object> mInstances = new Dictionary<Type, object>();

        /// <summary>
        /// 注册
        /// </summary>
        /// <param name="instance"></param>
        /// <typeparam name="T"></typeparam>
        public void Register<T>(T instance)
        {
            var key = typeof(T);

            if (mInstances.ContainsKey(key))
            {
                mInstances[key] = instance;
            }
            else
            {
                mInstances.Add(key,instance);
            }
        }

        /// <summary>
        /// 获取
        /// </summary>
        public T  Get<T>() where T : class
        {
            var key = typeof(T);
            
            object retObj;
            
            if(mInstances.TryGetValue(key,out retObj))
            {
                return retObj as T;
            }

            return null;
        }
    }
}
  1. 接口的阉割技术
  • 接口的显式与隐式实现
using UnityEngine;

namespace FrameworkDesign.Example
{
    /// <summary>
    /// 1. 定义接口
    /// </summary>
    public interface ICanSayHello
    {
        void SayHello();
        void SayOther();
    }
    
    public class InterfaceDesignExample : MonoBehaviour,ICanSayHello
    {
        /// <summary>
        /// 接口的隐式实现
        /// </summary>
        public void SayHello()
        {
            Debug.Log("Hello");
        }

        /// <summary>
        /// 接口的显式实现
        /// </summary>
        void ICanSayHello.SayOther()
        {
            Debug.Log("Other");
        }
        
        // Start is called before the first frame update
        void Start()
        {
            // 隐式实现的方法可以直接通过对象调用
            this.SayHello();
            
            // 显式实现的接口不能通过对象调用
            // this.SayOther() // 会报编译错误
            
            // 显式实现的接口必须通过接口对象调用
            (this as ICanSayHello).SayOther();
        }
    }
}
  • 接口-抽象类-实现类
  using UnityEngine;
  
  namespace FrameworkDesign.Example
  {
      public class InterfaceStructExample : MonoBehaviour
      {
          /// <summary>
          /// 接口
          /// </summary>
          public interface ICustomScript
          {
              void Start();
              void Update();
              void Destroy();
          }
          
          /// <summary>
          /// 抽象类
          /// </summary>
          public abstract class CustomScript : ICustomScript
          {
              protected bool mStarted { get; private set; }
              protected bool mDestroyed { get; private set; }
              
              /// <summary>
              /// 不希望子类访问 Start 方法,因为有可能破坏状态
              /// </summary>
              void ICustomScript.Start()
              {
                  OnStart();
                  mStarted = true;
              }
              
              void ICustomScript.Update()
              {
                 OnUpdate();
              }
  
              void ICustomScript.Destroy()
              {
                  OnDestroy();
                  mDestroyed = true;
              }
              
              /// <summary>
              /// 希望子类实现 OnStart 方法
              /// </summary>
              protected abstract void OnStart();
              protected abstract void OnUpdate();
              protected abstract void OnDestroy();
          }
          
          /// <summary>
          /// 由用户扩展的类
          /// </summary>
          public class MyScript : CustomScript
          {
              protected override void OnStart()
              {
                  Debug.Log("MyScript:OnStart");
              }
  
              protected override void OnUpdate()
              {
                  Debug.Log("MyScript:OnUpdate");
              }
  
              protected override void OnDestroy()
              {
                  Debug.Log("MyScript:OnDestroy");
              }
          }
  
          /// <summary>
          /// 测试脚本
          /// </summary>
          private void Start()
          {
              ICustomScript script = new MyScript();
              script.Start();
              script.Update();
              script.Destroy();
          }
      }
  }
  • 接口-静态扩展
  using UnityEngine;
  
  namespace FrameworkDesign.Example
  {
      public class CanDoEveryThing 
      {
          public void DoSomething1()
          {
              Debug.Log("DoSomething1");
          }
  
          public void DoSomething2()
          {
              Debug.Log("DoSomething2");
          }
  
          public void DoSomething3()
          {
              Debug.Log("DoSomething3");
          }
      }
  
      public interface IHasEveryThing
      {
          CanDoEveryThing CanDoEveryThing { get; }
      }
  
      public interface ICanDoSomething1 : IHasEveryThing
      {
          
      }
  
      public static class ICanDoSomeThing1Extensions
      {
          public static void DoSomething1(this ICanDoSomething1 self)
          {
              self.CanDoEveryThing.DoSomething1();
          }
      }
  
      public interface ICanDoSomething2 : IHasEveryThing
      {
          
      }
      
      public static class ICanDoSomeThing2Extensions
      {
          public static void DoSomething2(this ICanDoSomething2 self)
          {
              self.CanDoEveryThing.DoSomething2();
          }
      }
  
      public interface ICanDoSomething3 : IHasEveryThing
      {
  
      }
      
      public static class ICanDoSomeThing3Extensions
      {
          public static void DoSomething3(this ICanDoSomething3 self)
          {
              self.CanDoEveryThing.DoSomething3();
          }
      }
  
      public class InterfaceRuleExample : MonoBehaviour
      {
          public class OnlyCanDo1 : ICanDoSomething1
          {
              CanDoEveryThing IHasEveryThing.CanDoEveryThing { get; } = new CanDoEveryThing();
          }
          
          public class OnlyCanDo23 : ICanDoSomething2,ICanDoSomething3
          {
              CanDoEveryThing IHasEveryThing.CanDoEveryThing { get; } = new CanDoEveryThing();
          }
  
          private void Start()
          {
              var onlyCanDo1 = new OnlyCanDo1();
              // 可以调用 DoSomething1
              onlyCanDo1.DoSomething1();
              // 不能调用 DoSomething2 和 3 会报编译错误
              // onlyCanDo1.DoSomething2();
              // onlyCanDo1.DoSomething3();
  
  
              var onlyCanDo23 = new OnlyCanDo23();
              // 不可以调用 DoSomething1 会报编译错误
              // onlyCanDo23.DoSomething1();
              // 可以调用 DoSomething2 和 3
              onlyCanDo23.DoSomething2();
              onlyCanDo23.DoSomething3();
          }
      }
  }