《CounterApp》支持数据存储 我们尝试在CounterApp中引入数据存储功能,思路很简单,在CounterModel
构造时写入PlayerPrefs即可
修改CounterViewController
内的CounterModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class CounterModel : ICounterModel { public CounterModel () { Count.Value = PlayerPrefs.GetInt("COUNTER_COUNT" , 0 ); Count.OnValueChanged += count => { PlayerPrefs.SetInt("COUNTER_COUNT" , count); }; } public BindableProperty<int > Count { get ; } = new BindableProperty<int >() { Value = 0 }; }
这样就OK了,运行时存储的数据会被保留下来,每次点击运行时会恢复
顺便修改一下EditorCounterApp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using UnityEngine;using UnityEditor;namespace CounterApp.Editor { public class EditorCounterApp : EditorWindow { [MenuItem("EditorCounterApp/Open" ) ] static void Open () { var window = GetWindow<EditorCounterApp>(); window.position = new Rect(100 , 100 , 400 , 600 ); window.titleContent = new GUIContent(nameof (EditorCounterApp)); window.Show(); } private void OnGUI () { if (GUILayout.Button("+" )) new AddCountCommand().Execute(); GUILayout.Label(CounterApp.Get<ICounterModel>().Count.Value.ToString()); if (GUILayout.Button("-" )) new SubCountCommand().Execute(); } } }
打开时显示的也是相同的存储数字
我们不想让EditorCounterApp和CounterApp用相同的存储位置,而是EditorCounterApp使用EditorPrefs。
我们像上一节使用DIPExample的一样,先实现一个IStorage接口,然后创建两个实现类,我们先把它理解成数据存储模块,像CounterModel
一样的级别(其实不能这么理解,因为IStorage引用了CounterModel里面的数据才能存储)
在CounterApp——Script里面新建IStorage
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using UnityEngine;#if UNITY_EDITOR using UnityEditor;#endif namespace CounterApp { public interface IStorage { void SaveInt (string key, int value ) ; int LoadInt (string key, int defauleValue = 0 ) ; } public class PlayerPrefsStorage : IStorage { public int LoadInt (string key, int defauleValue = 0 ) { return PlayerPrefs.GetInt(key, defauleValue); } public void SaveInt (string key, int value ) { PlayerPrefs.SetInt(key, value ); } } public class EditorPrefsStorage : IStorage { public int LoadInt (string key, int defauleValue = 0 ) { #if UNITY_EDITOR return EditorPrefs.GetInt(key, defauleValue); #else return 0 ; #endif } public void SaveInt (string key, int value ) { #if UNITY_EDITOR EditorPrefs.SetInt(key, value ); #endif } } }
然后在CounterApp
中注册这个模块,先用PlayerPrefsStorage
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 using FrameWorkDesign;namespace CounterApp { public class CounterApp : Architecture <CounterApp > { protected override void Init () { Register<ICounterModel>(new CounterModel()); Register<IStorage>(new PlayerPrefsStorage()); } } }
出现递归调用 接下来会出现问题,如果我们修改CounterModel
的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class CounterModel : ICounterModel { public CounterModel () { var storage = CounterApp.Get<IStorage>(); Count.Value = storage.LoadInt("COUNTER_COUNT" , 0 ); Count.OnValueChanged += count => { storage.SaveInt("COUNTER_COUNT" , count); }; } public BindableProperty<int > Count { get ; } = new BindableProperty<int >() { Value = 0 }; }
我们的Architecture类,也就是CounterApp是类似于单例一样的函数,我们启动时
调用CounterApp.Get——此时Architecture静态引用还没创建——调用MakeSureArchitecture()——调用CounterApp.Init()——调用Register<ICounterModel>(new CounterModel())——CounterModel在构造的时候会调用CounterApp.Get<IStorage>()——此时Architecture静态引用还没创建——调用MakeSureArchitecture()——调用CounterApp.Init()——调用Register<ICounterModel>(new CounterModel())……进入递归调用
出现这个问题的根本原因,就是CounterModel和Storage因为上面的构造函数代码而互相嵌套了,在以单例为主要模型的框架中,这种情况要极力避免,其实避免将模块互相写在构造里就好
但是在本例中,我们的目的是数字每次发生变化的时候一定会自动存储,所以storage在CounterModel构造的时候获取是最合理的,那么我们不要把Storage类和CounterModel类看作平级,换个思路来解决这个问题
引入IBelongToArchitecture接口 在FrameworkDesign——Framework——Architecture文件夹中新建IBelongToArchitecture
脚本
1 2 3 4 5 6 7 namespace FrameWorkDesign { public interface IBelongToArchitecture { IArchitecture Architecture { get ; set ; } } }
其中的IArchitecture
接口我们没有定义,我们在Architecture
脚本中定义它,并且让Architecture类继承它并实现其中定义的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace FrameWorkDesign { public interface IArchitecture { T GetUtility <T >() where T : class ; } public abstract class Architecture <T > : IArchitecture where T : Architecture <T >,new () { public void RegisterModel <T >(T model ) where T : IBelongToArchitecture { model.Architecture = this ; m_Container.Rigister<T>(model); } public T GetUtility <T >() where T : class { return m_Container.Get<T>(); } } }
这样写的目的
使用IArchitecture
为Architecture基类提供一层抽象标记
使用IBelongToArchitecture
为Model类(如CounterModel)再提供一层抽象标记(第一层是ICounterModel),并声明IArchitecture
属性在Model类中得到其Architecture基类的引用
IArchitecture中的GetUtility方法就是为了在Model中能够访问Utility,也就是说解决之前Storage类和CounterModel类看作平级的问题,把Storage类看作Utility
在之前我们用CounterApp.Get
获取Storage,其中Get是静态方法,这里我们的GetUtility
不是静态方法,所以使用属性引用的方式,而且也不调用MakeSureArchitecture
。这是为了确保像CounterApp这样的Architecture子类不能直接调用GetUtility
,只在Model类中调用GetUtility,更直接的原因是CounterApp类只能注册Model,而Model调用Utility要绕过CounterApp直接从Architecture基类中获取
添加RegisterModel方法,在注册Model时自然将Architecture基类放进其声明的属性里面
修改CounterModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public interface ICounterModel : IBelongToArchitecture { BindableProperty<int > Count { get ; } } public class CounterModel : ICounterModel { public IArchitecture Architecture { get ; set ; } public CounterModel () { var storage = Architecture.GetUtility<IStorage>(); Count.Value = storage.LoadInt("COUNTER_COUNT" , 0 ); Count.OnValueChanged += count => { storage.SaveInt("COUNTER_COUNT" , count); }; } public BindableProperty<int > Count { get ; } = new BindableProperty<int >() { Value = 0 }; }
修改CounterApp
的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 using FrameWorkDesign;namespace CounterApp { public class CounterApp : Architecture <CounterApp > { protected override void Init () { RegisterModel<ICounterModel>(new CounterModel()); Register<IStorage>(new PlayerPrefsStorage()); } } }
这里还是有一个问题,我们在CounterModel的构造中直接调用了Architecture.GetUtility<IStorage>()
,但是按照我们目前的逻辑,我们调用到RegisterModel<ICounterModel>(new CounterModel());
时,IStorage还没有被注册进去,所以我们必须想办法先保证各个Model和Utility都注册完毕,再在各个Model中Get到Utility等
引入IModel接口 在FrameworkDesign——Framework——Architecture文件夹中新建IModel
脚本
1 2 3 4 5 6 7 namespace FrameWorkDesign { public interface IModel : IBelongToArchitecture { void Init () ; } }
可以看到这是再次把Model类抽象一次,并要求提供Init方法
再次修改Architecture
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 using System.Collections.Generic;using UnityEngine;namespace FrameWorkDesign { public interface IArchitecture { T GetUtility <T >() where T : class ; } public abstract class Architecture <T > : IArchitecture where T : Architecture <T >,new () { private bool m_Inited = false ; private List<IModel> m_Models = new List<IModel>(); private static T m_Architecture; static void MakeSureArchitecture () { if (m_Architecture == null ) { m_Architecture = new T(); m_Architecture.Init(); foreach (var architectureModel in m_Architecture.m_Models) { architectureModel.Init(); } m_Architecture.m_Models.Clear(); m_Architecture.m_Inited = true ; } } protected abstract void Init () ; private IOCContainer m_Container = new IOCContainer(); public static T Get <T >() where T : class { MakeSureArchitecture(); return m_Architecture.m_Container.Get<T>(); } public void Register <T >(T instance ) { MakeSureArchitecture(); m_Architecture.m_Container.Rigister<T>(instance); } public void RegisterModel <T >(T model ) where T : IModel { model.Architecture = this ; m_Container.Rigister<T>(model); if (!m_Inited) { m_Models.Add(model); } else { model.Init(); } } public T GetUtility <T >() where T : class { return m_Container.Get<T>(); } } }
然后我们再次修改CounterModel
部分的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public interface ICounterModel : IModel { BindableProperty<int > Count { get ; } } public class CounterModel : ICounterModel { public IArchitecture Architecture { get ; set ; } public void Init () { var storage = Architecture.GetUtility<IStorage>(); Count.Value = storage.LoadInt("COUNTER_COUNT" , 0 ); Count.OnValueChanged += count => { storage.SaveInt("COUNTER_COUNT" , count); }; } public BindableProperty<int > Count { get ; } = new BindableProperty<int >() { Value = 0 }; }
这里我们增加的IModel.Init
方法称为声明周期方法,它是避免循环调用造成堆栈溢出的解决方案之一,具体表现为在Architecture.MakeSureArchitecture
方法中,IModel.Init
方法在Architecture.Init
方法之后。这样在Model中调用Utility不依赖于静态的Get方法(CounterApp.Get
),而时通过Architecture属性获取Architecture基类的实例,然后调用GetUtility
方法(GetUtility
方法不是静态的),在Architecture.Init
调用时,会调用RegisterModel
方法,必定会保证Architecture属性不是空引用。
这时运行CounterApp,就不会造成堆栈溢出的问题了
实现EditorCounterApp 我们继续修改Architecture
脚本,让它更规范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using System.Collections.Generic;using System;namespace FrameWorkDesign { public interface IArchitecture { T GetUtility <T >() where T : class ; void RegisterModel <T >(T model ) where T : IModel ; void RegisterUtility <T >(T isntance ) ; } public abstract class Architecture <T > : IArchitecture where T : Architecture <T >,new () { public static Action<T> OnRegisterPatch = architecture => { }; static void MakeSureArchitecture () { if (m_Architecture == null ) { m_Architecture = new T(); m_Architecture.Init(); OnRegisterPatch?.Invoke(m_Architecture); foreach (var architectureModel in m_Architecture.m_Models) { architectureModel.Init(); } m_Architecture.m_Models.Clear(); m_Architecture.m_Inited = true ; } } public static void Register <T >(T instance ) { MakeSureArchitecture(); m_Architecture.m_Container.Rigister<T>(instance); } public void RegisterUtility <T >(T utility ) { m_Container.Rigister<T>(utility); } } }
修改CounterApp
代码,使用我们新实现的RegisterUtility
1 2 3 4 5 6 7 8 9 10 11 12 13 using FrameWorkDesign;namespace CounterApp { public class CounterApp : Architecture <CounterApp > { protected override void Init () { RegisterModel<ICounterModel>(new CounterModel()); RegisterUtility<IStorage>(new PlayerPrefsStorage()); } } }
然后修改EditorCounterApp
代码,将其需要重新设置的构造打入补丁中,Patch即补丁的意思
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using UnityEngine;using UnityEditor;namespace CounterApp.Editor { public class EditorCounterApp : EditorWindow { [MenuItem("EditorCounterApp/Open" ) ] static void Open () { CounterApp.OnRegisterPatch += app => { app.RegisterUtility<IStorage>(new EditorPrefsStorage()); }; var window = GetWindow<EditorCounterApp>(); window.position = new Rect(100 , 100 , 400 , 500 ); window.titleContent = new GUIContent(nameof (EditorCounterApp)); window.Show(); } private void OnGUI () { if (GUILayout.Button("+" )) new AddCountCommand().Execute(); GUILayout.Label(CounterApp.Get<ICounterModel>().Count.Value.ToString()); if (GUILayout.Button("-" )) new SubCountCommand().Execute(); } } }
这里打补丁是因为如果我们直接启用EditorCounterApp,会调用一遍MakeSureArchitecture
流程,如果不打补丁的话每次流程过后构造出来的都是PlayerPrefsStorage
,除非我们先启动游戏,再打开EditorCounterApp窗口,这样就不用打补丁了,直接调用CounterApp.Register<IStorage>(new EditorPrefsStorage())
即可,因为我们启动游戏后初始化已经完成,这时再打开Editor窗口就会重新设置构造
我们到目前,引入了Utility层,更新一下CounterApp的架构图