《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>();//缓存要初始化的Model

private static T m_Architecture;

static void MakeSureArchitecture()
{
if(m_Architecture == null)
{
m_Architecture = new T();
m_Architecture.Init();//用这个注册各种Model和Utility

foreach (var architectureModel in m_Architecture.m_Models)
{
architectureModel.Init();//用这个在Model中GetUtility或添加监听
}

m_Architecture.m_Models.Clear();//Model真正的存储位置是在m_Container里,这个列表仅仅是为了初始化时统一调用
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()//删除构造,在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();//Model真正的存储位置是在m_Container里,这个列表仅仅是为了初始化时统一调用
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的架构图

CunterApp with Utility