2019-5-29-CLX教程(二)面向对象封装的简单应用 数据与事件

CLX course(2) OOT

Posted by BY LX on May 29, 2019

主要内容

前言

Show You The Code

结语

正文

1. 前言

前一篇文章谈到了如何简单的封装数据,即: 在数据交互的时候,套上一层逻辑

然后让我们接着思考,之前的代码还有什么缺陷。

我认为目前所有的缺陷如下:

  • 只针对特定一种变量的变化(即HP的情况)而发起事件,功能单一。
  • 关于事件的注册与执行有重复代码。

现在,我们针对这些问题来进行一下优化。

2. Show You The Code

首先,我们来解决第一点,即:只针对特定一种变量的变化(即HP的情况)而发起事件,功能单一。

那么这个单一的意义包含哪些东西呢?我们来分析一下:首先,我们的HP变量意义太过明确,即就是一个代表HP的数值,那么,如果我们想针对数据写一个通用的封装呢,比如说, 如果一个数据的值发生改变的时候,发起某个事件 这时候我想到了 泛型

于是我写下了这些代码:

namespace CLX.TEST
{
    /// <summary>
    /// 缺点:只有在变量值的值发生改变了的时候才会调用函数,功能太单一
    /// 待完善点:只有注册,没有删除
    /// 注意点:使用的数据需要覆写Equal方法
    /// </summary>
    /// <typeparam name="T">保存数据的类型</typeparam>
    public class EventVariable<T>
    {
        private T mValue;
        private Action<T, T> mOnValueChanged;

        public EventVariable(T val, Action<T, T> onValueChanged = null)
        {
            mValue = val;
            mOnValueChanged = onValueChanged;
        }

        public void RegisterValueChangeEvent(Action<T, T> onValueChanged)
        {
            if (mOnValueChanged != null)
            {
                mOnValueChanged += onValueChanged;
            }
            else
            {
                mOnValueChanged = onValueChanged;
            }
        }

        public T GetValue()
        {
            return mValue;
        }

        public void SetValue(T value)
        {
            if (value.Equals(mValue) == false)
            {
                mOnValueChanged?.Invoke(value, mValue);
            }
            mValue = value;
        }

    }
}

public class EventVariableTest : MonoBehaviour
{
    EventVariable<int> eventVariable;

    private IEnumerator Start()
    {
        eventVariable = new EventVariable<int>(5, (newValue, oldValue) =>
        {
            Debug.Log("NewValue:" + newValue + "\nOldValue:" + oldValue);
        });

        yield return new WaitForSeconds(1f);
        eventVariable.SetValue(5);

        yield return new WaitForSeconds(1f);
        eventVariable.SetValue(6);

        yield return new WaitForSeconds(1f);
        eventVariable.SetValue(4);
    }
}

我们使用了泛型后,我们发现这些代码的适用场景明显变多了,也就是说: 我们提高了代码的可复用性

但是,这儿仍存有不少问题,我列在下面:

  • 事件只有注册,没有删除的代码。
  • 只有在变量值的值发生改变了的时候才会调用函数,功能太单一。

然而,没有删除这并不影响我们今天要讨论的话题,而且这个实现起来较为简单,故先不提。

我们关注一点,也就是 只有在变量值的值发生改变了的时候才会调用函数,功能太单一。 这句话是什么意思呢?也就是说,如果我们要存储的那些数据有一些特定的值,然后如果是这些特殊的值的时候,我们要执行一些特殊的行为。比如说,我们想在玩家血量<=0的时候,让玩家进行死亡事件,并且让玩家的HP = 0。

我们发现,使用目前的代码实现这样的功能较为复杂,且可能让代码集中在一处,或是产生不利于我们的理解与阅读。

而如果再加入一些单独的逻辑的话,就会破坏掉之前写的结构,或者造成代码的重复。

那么我们目前的目的就是: 让我们处理一些特殊情况的数据更加方便、清晰,且避免之后的逻辑破坏原有结构与造成重复代码

我们想到,如果可以让数据进行一些可变化的判断逻辑再来判断是否要执行某些逻辑以及这些数据是否合法这样就可以大大提高我们程序的灵活性。

于是,我针对上面的问题写了如下代码:

namespace CLX.TEST
{
    public class EventsVariable<T>
    {
        private T mValue;
        /// <summary>
        /// 为两个返回值类型为:bool 参数类型为(T,T)的函数组成的键值对
        /// 由于是键值对,故称之为Key函数和Value函数
        /// 两个函数的参数作用都是:第一个T是改变后的值,第二个T是改变前的值
        /// Key函数的bool类型返回值的意思是,根据传入参数判断是否该执行Value函数
        /// Value函数的bool类型返回值意思是,根据传入参数得到是否应该去改变mValue的值,过滤掉一些不合法的赋值,若为true则合法,可正常赋值,若为false则不可赋值
        /// </summary>
        private Dictionary<Func<T, T, bool>, Func<T, T, bool>> mOnValChangeEvents;

        public T GetValue()
        {
            return mValue;
        }

        public void SetValue(T value)
        {
            bool shouldChange = true;
            foreach (var pair in mOnValChangeEvents)
            {
                if (pair.Key(value, mValue))
                {
                    shouldChange &= pair.Value(value, mValue);
                }
            }
            if (shouldChange)
            {
                mValue = value;
            }
        }

        public EventsVariable(T value)
        {
            mOnValChangeEvents = new Dictionary<Func<T, T, bool>, Func<T, T, bool>>();
            mValue = value;
        }

        public void RegisterEvent(Func<T, T, bool> key, Func<T, T, bool> value)
        {
            if (key == null || value == null)
            {
                throw new NullReferenceException("Key或者Value为空");
            }
            mOnValChangeEvents.Add(key, value);
            if (key(mValue, mValue))
            {
                value(mValue, mValue);
            }
        }
    }
}

public class EventsvariableTest : MonoBehaviour
{
    EventsVariable<int> hp;
    int maxVal = 10;
    Slider slider;
    
    // Start is called before the first frame update
    IEnumerator Start()
    {
        slider = GetComponent<Slider>();
        hp = new EventsVariable<int>(maxVal);
        /// 注册一个血量判断的事件
        /// 当血量小于等于0的时候
        /// 将血量赋值为0
        hp.RegisterEvent((nowVal, oldVal) =>
        {
            if (nowVal < 0)
            {
                return true;
            }
            return false;
        }, (nowVal, oldVal) =>
        {
            Debug.Log("血量小于0,重新赋值为0");
            hp.SetValue(0);
            return false;
        });

        hp.RegisterEvent((nowVal, oldVal) =>
        {
            if (nowVal >= 0) return true;
            return false;
        }, (nowVal, oldVal) =>
        {
            slider.value = (float)nowVal / maxVal;
            Debug.LogFormat("\nNow Value:{0}\nOld Value:{1}", nowVal, oldVal);
            return true;
        });

        for(int i = 0; i < maxVal + 3; i++)
        {
            yield return new WaitForSeconds(1f);
            hp.SetValue(hp.GetValue() - 1);
        }
    }
}

如果执行这些代码会产生如下结果:

运行结果

这样以后,其实对于数据的改变的处理已经有了很大的灵活性,我们之前提到的问题,也基本上都得到了解决。

3. 结语

其实解决今天我所提出的问题的关键是在于C#语言中的委托机制,其实在其他语言中也有类似的机制,比如说其他各种语言里的匿名函数。

其核心思想就是将一些 逻辑当成对象来对待 ,这样的话我们便可以使用数据结构来保存,或者像对象一样的传来传去。比如说我们使用Dictionary来保存条件函数与处理函数与Register函数里的委托参数。

另外,究其根本,我们愿意去写、去优化这些看似毫无必要的代码的意义在于: 提高优质代码的可复用性

我所认为优质代码的意义包含:

  • 灵活性高。
  • 对于代码逻辑的考量比较全面。
  • 应用的方面广。

而复用这些代码事实上就是避免去重复造轮子的过程。

而我们写这些代码的意义就在于,去制定了一套规则,以后使用这套规则可以大大的节省我们的工作量。

举个例子,如果我们不去写这些代码,我们去写一个变量根据特殊值、根据数值变化会执行某些逻辑,我们要写哪些内容:

  • 各特殊值的if语句罗列
  • 各特殊值的事件执行
  • 各特殊值事件的注册
  • 数值变化的事件注册
  • 数值变化的事件执行

这其中最关键的其实是各特殊值事件的注册,因为特殊值可能有很多,针对每一种特殊值去进行注册的话势必会产生大量重复代码。

然后我们分析下使用今天写下的代码写要写哪些东西:

  • 判断特殊值事件的注册
  • 根据特殊值进行的事件的注册

这样就少了很多的步骤,且能让代码更加清晰明了。

同样的,我也将示例代码上传到了 Github ,大家感兴趣的可以导入运行进行学习。

感谢支持!