项目应用的设计模式
项目为什么要用到设计模式?
如果是单人开发,其实完全可以瀑布式开发,因为就你自己开发,到后期解不开耦合的时候也难受的也是你自己哈哈。
再比如之前挺火的一个独立游戏<<太吾绘卷>> 是一个策划hardcode出10W加if else 做出来的一个游戏(据说)。其实说这些都是告诉大家,项目不用设计模式一样能做出东西,只是会有后续问题。
下面我们思考一个问题。如果作者想做一个类似的游戏,他们现在的代码几乎是不能服用,完全再重写一遍。
再思考一个问题,如果来一个新人,先拓展功能,估计不把这个10W+的if else看明白都不动。 我举的这个例子就是抛砖引玉,一个项目架构的重要性。
设计模式六大原则
目前不管是现有流行的设计模式还是我们不知道的设计模式,其实根本的都是围绕这个六大原则来的。
- 单一职责原则(类和方法,接口)
- 开闭原则(扩展开放,修改关闭)
- 里氏替换原则(基类和子类之间的关系)
- 依赖倒置原则(依赖抽象接口,而不是具体对象)
- 接口隔离原则(接口按照功能细分)
- 迪米特法则(类与类之间的亲疏关系,又称最少知道原则)
用流程图简单讲解一下单一职责
public class Student {
public void Live();
public void Game();
public void Learn();
public void 特长();
public void home();
public void 文化学习();
}
- 单一职责 开闭原则
public class Student :Inteface {
public void Live();
public void Game();
public void Learn();
//用了那个接口就要去实现接口方法即可
//
//
//
}
public Inteface Istudent特长
{
public void 特长();
}
public Inteface Istudent文化学习
{
public void 文化学习();
}
public Inteface Istudenthome
{
public void home();
}
- 里氏替换原则
1.里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范
2.里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
继续用一个经理的悖论来解释一下.“正方形不是矩形”
public class Rectangle {
int length;
int width;
public void setLength(int length) {
this.length = length;
}
public void setWidth(int width) {
this.width = width;
}
public int getArea() {
return length * width;
}
}
class Square extends Rectangle {
public void setLength(int length) {
this.length = length;
this.width = length;
}
public void setWidth(int width) {
this.width = width;
this.length = width;
}
}
public class Main {
public static void main(String[] args) {
Square square = new Square();
System.out.println(test(square)); // false
}
public static boolean test(Rectangle rectangle) {
rectangle.setLength(4);
rectangle.setWidth(3);
return rectangle.getArea() == 4 * 3; // Oops... 9 != 12
}
}
问题出现了,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是矩形。
重写抽象矩形类作为父类,并实现 Figure 接口这个接口的功能是求面积。求面积这个功能并不是矩阵本身具有的,而是它作为一个封闭图形能够实现的功能,所以需要用接口来实现
interface IClosedFigure {
int getArea();
}
abstract class AbstractRectangle implements IClosedFigure {
abstract int getLength();
abstract int getWidth();
}
class Rectangle extends AbstractRectangle implements IClosedFigure {
private final int length;
private final int width;
Rectangle(int length, int width) {
this.length = length;
this.width = width;
}
override int getLength() {
return length;
}
override int getWidth() {
return width;
}
override public int getArea() {
return length * width;
}
}
class Square extends AbstractRectangle implements IClosedFigure {
private final int sideLength;
Square(int sideLength) {
this.sideLength = sideLength;
}
override int getLength() {
return sideLength;
}
override int getWidth() {
return sideLength;
}
override public int getArea() {
return sideLength * sideLength;
}
}
正文
绕了一大圈终于说到正文了。下面我们来看一下项目用的设计模式
首先单例模式
public class UIManager : MonoBehaviour
{
public static UIManager instance;
private void Awake()
{
if (instance == null)
{
instance = this;
}
}
}
public class GameMananger : MonoBehaviour
{
//单例
public static GameMananger instance;
private void Awake()
{
if (instance == null)
{
instance = this;
}
DontDestroyOnLoad(this);
}
}
发现有很多重复的代码,所以我们进一步优化
public class Singleton<T> : MonoBehaviour where T: MonoBehaviour //因为继承MonoBehaviour的物体不能使用new()来构造实例 所以这里只能用MonoBehaviour
{
private static T instance; //实例目标对象
protected virtual void Awake()
{
this.CheckInstance(); //觉醒检查
}
protected bool CheckInstance()//是否唯一,不是就销毁多余的目标,防止目标有多个挂载在其他gameobject上。
{
if (this == Singleton<T>.Instance)
{
return true;
}
UnityEngine.Object.Destroy(this);
return false;
}
public static T Instance
{
get
{
if (Singleton<T>.instance == null)
{
Singleton<T>.instance = (T) UnityEngine.Object.FindObjectOfType(typeof(T));// 如果对象没实例化,则为空
if (Singleton<T>.instance == null)
{
Debug.LogError(typeof(T) + " was no attached GameObject");
}
}
return Singleton<T>.instance;
}
}
}
public class UIManager : Singleton<T>
{
//实现自己的业务逻辑即可
}
public class GameMananger : Singleton<T>
{
//实现自己的业务逻辑即可
}
Main()
{
UIManager.instance.业务逻辑();
}
这样就实现了一个泛型单例。但是等项目的复杂度上来之后你还会发现这样的写法会增加其他人的心智负担。他还需要去理解。并且每一个单例的初始化有点混乱。
所以我们继续再继续优化 实现一个IOC容器
public static class IOCContainer
{
private Dictionary<Type, object> mInstance = new Dictionary<Type, object>();
public void Register<T>(T instance)
{
var key = typeof(T);
if (mInstance.ContainsKey(key))
{
mInstance[key] = instance;
}
else
{
mInstance.Add(key, instance);
}
}
public T Get<T>() where T : class
{
var key = typeof(T);
if (mInstance.TryGetValue(key, out var retInstanve))
{
return retInstanve as T;
}
return null;
}
}
main()
{
//注册
IOCContainer .Register(GameMananger );
IOCContainer .Register(UIManager );
IOCContainer .Register(...);
IOCContainer .Register(...);
IOCContainer .Register(...);
//使用
IOCContainer .Get(GameMananger );
IOCContainer .Get(UIManager );
IOCContainer .Get(...);
IOCContainer .Get(...);
}
解耦合
多人协同开发的话高聚合低耦合重要性不言而喻。
但是高聚合低耦合这话有点抽象,我就粗略的概括一下就是 强引用关系和弱引用关系。
当一个类持有了另一个类的对象,就是引用了(属于单向引用),如果这俩个类互相持有对方的对象,则这种引用就是很强了(双向引用),一般中强引用关系,放大大型复杂的项目里,必然会造成后续开发者的心智负担,而且健壮性极差。完全不符合了咱们上面说的SOLLID原则。
直接进入正题,对于这种解耦合的方式,咱们项目的方案是用事件委托来解耦的
上代码
/// <summary>
/// 事件参数
/// </summary>
/// <param name="key"></param>
/// <param name="param"></param>
public delegate void EventDelegate(params object[] param);
/// <summary>
/// 成员方法接口
/// </summary>
public interface IEventMgr
{
void Register(ENventType key, EventDelegate eventMgr);//注册事件
void UnRegister(ENventType key);//解绑事件
void ClearAll();//解绑所有事件
bool IsRegisterName(ENventType key);//key值是否被注册
bool IsRegisterFunc(EventDelegate eventMgr);//eventMgr是否被注册
void Invoke(ENventType key, params object[] param);//调用
void Succeessc(ENventType key, params object[] param);//调用
}
/// <summary>
/// des:事件管理
/// </summary>
public class EventManager : MonoBehaviour, IEventMgr
{
//单例
public static EventManager instance;
private void Awake()
{
if (instance == null)
{
instance = this;
}
}
/// <summary>
/// 存储注册好的事件
/// </summary>
protected readonly Dictionary<ENventType, EventDelegate> EventListerDict = new Dictionary<ENventType, EventDelegate>();
/// <summary>
/// 是否暂停所有的事件
/// </summary>
public bool IsPause = false;
/// <summary>
/// 注册事件
/// </summary>
/// <param name="key"></param>
/// <param name="eventMgr"></param>
public void Register(ENventType key, EventDelegate eventMgr)
{
if (EventListerDict.ContainsKey(key))
{
Debug.LogError("Key:" + key + "已存在!");
}
else
{
EventListerDict.Add(key, eventMgr);
}
}
/// <summary>
/// 取消事件绑定
/// </summary>
/// <param name="key"></param>
public void UnRegister(ENventType key)
{
if (EventListerDict != null && EventListerDict.ContainsKey(key))
{
EventListerDict.Remove(key);
Debug.Log("移除事件:" + key);
}
else
{
Debug.LogError("Key:" + key + "不存在!");
}
}
/// <summary>
/// 取消所有事件绑定
/// </summary>
public void ClearAll()
{
if (EventListerDict != null)
{
EventListerDict.Clear();
Debug.Log("清空注册事件!");
}
}
/// <summary>
/// ID是否注册过
/// </summary>
/// <param name="key"></param>
public bool IsRegisterName(ENventType key)
{
if (EventListerDict != null && EventListerDict.ContainsKey(key))
{
EventListerDict.Remove(key);
Debug.Log("事件:" + key + "已注册!");
return true;
}
Debug.Log("事件:" + key + "未注册!");
return false;
}
/// <summary>
/// 方法是否注册过
/// </summary>
/// <param name="eventMgr"></param>
public bool IsRegisterFunc(EventDelegate eventMgr)
{
if (EventListerDict != null && EventListerDict.ContainsValue(eventMgr))
{
Debug.Log("事件已注册!");
return true;
}
Debug.Log("事件未注册!");
return false;
}
/// <summary>
/// 调用事件
/// </summary>
/// <param name="key"></param>
/// <param name="param"></param>
public void Invoke(ENventType key, params object[] param)
{
// new AddCommand().Execute();
if (!IsPause)
{
if (EventListerDict.ContainsKey(key))
{
EventListerDict[key].Invoke(param);
//所有的委托类型,编译器都会自动生成一个 invoke 方法.
//用委托类型直接加参数是Invoke(参数)的一个捷径.
// 其实等价调用 Invoke();
// EventListerDict[key](param);
}
else
{
Debug.LogError("事件:" + key + "未注册!");
}
}
else
{
Debug.LogError("所有事件已暂停!");
}
}
}
调用的地方
class LoginComponent
{
void Start()
{
.....
EventManager.instance.Register(ENventType.LOGINUI,ShowLoginUI);
}
}
private void ShowLoginUI(params object[] param)
{
//param 参数
//业务逻辑
}
...
...
private void Start()
{
EventManager.instance.Register(ENventType.CLOSEPANEL, UIPanleClose);
}
private void OnClickBuild()
{
print("OnClickBuild");
// GameMananger.instance.BuildCameraOpenAndClose();
EventManager.instance.Invoke(ENventType.CameraEvent, false);
EventManager.instance.Invoke(ENventType.CLOSEPANEL,
UIManager.instance.chatPanelPath,UIManager.instance.homePath);
}
public void UIPanleClose(params object[] param)
{
GameObject obj;
foreach (var item in param)
{
if (uiPrfabDict.TryGetValue(item as string, out obj))
{
// print("AJCHEN UIMANAGER = " + item.ToString());
obj.SetActive(false);
}
}
}
上面是用事件委托去解耦的。但是,但是,这种写法虽然解耦了 仔细思考也是有不合理的地方,就是代码逻辑复杂的时候,不断的写事件发送,别人阅读的时候也会增加别的心智负担,但确实是解耦了。我就再想 还有没有更好的方案。答案是有的。
但是 但是 下面代码只是完成了设计和代码,项目中并没有用到,因为本人对这种方式理解尚欠,把握不住。
上代码
public class BindableProperty<T> where T : IEquatable<T>
{
public Action<T> OnValueChanged;
private T mValue;
public T Value
{
get => mValue;
set
{
if (!mValue.Equals(value))
{
mValue = value;
OnValueChanged?.Invoke(value);
}
}
}
}
上面是核心代码
public readonly BindableProperty<ViewModel> ViewModelProperty = new BindableProperty<ViewModel>();
public ViewModel BindingContext
{
get { return ViewModelProperty.Value; }
set { ViewModelProperty.Value = value; }
}
protected virtual void OnBindingContextChanged(ViewModel oldViewModel)
{
}
public UnityGuiView()
{
this.ViewModelProperty.OnValueChanged += OnBindingContextChanged;
}
protected override void OnBindingContextChanged(ViewModel valuel)
{
base.OnBindingContextChanged(oldViewModel, newViewModel);
}
这种设计-一般叫做数据驱动。
但是,又是但是。任何一种设计都有自己的缺点。这种数据绑定 只能一对一绑定。
所以 如果需要多参数的 我们用上面的事件委托。比如金币 ,砖石,等单数据用Binding数据驱动更好。
总结
从上面的讲解我们其实也能感受到,没有一个设计是万能的,都或多或少有自己身的问题,所以我们要根据项目情况来合理的设计。如上面的图。
在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。