没用的响应式编程?

Posted CZandQZ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了没用的响应式编程?相关的知识,希望对你有一定的参考价值。

    首先搜索一波响应式编程的概念。响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。多么难以理解的概念啊,举个例子说明一下。就是当我们在游戏中点击一个跳跃图标button的时候,玩家发生了跳跃动作,同时玩家的位置也发生了相应的变化,还有当角色等级提升的时候,攻击力,护甲等一系列属性也会发生相应的变化。最容易理解的例子就是杀死一个敌人得时候玩家的金币会发生增加,金币的变化是怎么引起的呢?而不只是简单的对金币的那个Text对应的String发生了变化。而是玩家model层里的一个变量CoinValue发生了变化所以才会引起金币Text上的更新,敌人死亡之后首先是控制层去通知model层的数据更新,说白了就是调用控制层修改模型层数据的方法而已,数据发生更新之后就是通知视图层发生变化。用mvc的思想代码如下:视图层的代码就只是一些UI的显示逻辑。代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;

namespace Assets.Scripts

    public class View : MonoBehaviour
    
        [SerializeField] private Text _coninText;

        public void FillCoinValue(int coinValue)
        
            _coninText.text = coinValue.ToString();
        
    

就只是单纯的对一个Text的填充而已。模型层的代码就需要对视图层进行绑定操作了,最简单的做法就是定义一个int类型属性,在它的Set里面判断值发生变化的时候执行一个委托,这个委托我们的需要在模型创建的时候就定义好这个委托,这个委托说白就是执行视图层填充那个Text方法。所以模型层代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Assets.Scripts

    public class Model
    

        private Action<int> _coinValueChangeAction;

        private int _coinValue;
        public int CoinValue
        
            set
            
                if (_coinValue != value)
                
                    _coinValue = value;
                    if(_coinValueChangeAction!=null)
                        _coinValueChangeAction.Invoke(_coinValue);
                

            
            get  return _coinValue; 
        




        public void BindView_CoinValue(Action<int> coinValueChangeAction)
        
            _coinValueChangeAction = coinValueChangeAction;
        

    


最后就是控制层,前面说了控制层就是简单对模型层的数据进行操作,这里例子就是简单的对金币的加减操作了。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace Assets.Scripts

    public class Controller
    

        private Model _model;
        public Controller(Model model)
        
            _model = model;
        


        public void AddCoin(int count)
        
            _model.CoinValue += count;
        

        public void ConsumCoin(int count)
        
            _model.CoinValue -= count;
        

            
    


接下来就是一个监听用户的判断了,我们将其定义为Player,在Player里面创建控制层和模型层,视图层直接用unity的UGUI来实现。在模型层创建之后我们同时需要让模型层是视图层产生关联,这个关联就是通过一个委托来实现的。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Assets.Scripts

    public class Player : MonoBehaviour
    

        [SerializeField] private View _view;

        private Controller _controller;
        private void Awake()
        
            Model model = new Model();
            model.BindView_CoinValue(t =>  _view.FillCoinValue(t); );
            _controller = new Controller(model);
        

        private void Update()
        
            if (Input.GetMouseButtonDown(0))
            
               _controller.AddCoin(2);
            
            else if (Input.GetMouseButtonDown(1))
            
                _controller.ConsumCoin(1);
            
        


    


在mvc里面控制层是不和模型层产生关联的。说了半天这里跟响应式编程有半毛钱的关系啊,在这里其实可以非常明显的可以看到整个过程都是数据发生了变化才会引起UI的变化的而不是,当然我们也可以以另外一种方式来理解MVC就是将视图层放到控制层里面那么控制层里面的代码的书写就应该是
       public void AddCoin(int count)
        
            _model.CoinValue += count;
            _view.FillCoinValue(_model.CoinValue);
        

        public void ConsumCoin(int count)
        
            _model.CoinValue -= count;
            _view.FillCoinValue(_model.CoinValue);
        
这样写代码肯定是没有问题的。问题就是感觉就是控制层是在控制模型和视图层来同时更新的。导致代码的耦合性太差了,你的控制层必须持有视图层的引用,如果我们UI不用UGUI了,那这里的代码就要发生变化了,我们本着这样的原则,我们应该是在它创建出来的时候进行约束替换,而不是改原来的代码。说白了就是如果视图层的代码发生了变化的时候这里改的代码不是改一处了,如果这个类中有50个方法,这里岂不是要改50次了,如果有一处没改过来就会导致错误的显示。如果用第一种让模型层和视图层之间保持一种弱弱的联系的话,这样改的话就只用找到相应的变量它所对应的UI更新即可。这样一种思想其实就是一种典型的响应式编程的方式,我们可以将这个CoinvValue当成一个被观察的对象,而UI更新就是一个观察者,条件出发(数值发生了变化)触发的时候观察者做出相应的反应(这里变化就是对应UI发生更新)。如果我们往模型层里面添加一个等级的时候。代码更改如下。
        private Action<int> _coinValueChangeAction;
        private Action<int> _levelValueChangeAction;

        private int _coinValue;
        public int CoinValue
        
            set
            
                if (_coinValue != value)
                
                    _coinValue = value;
                    if(_coinValueChangeAction!=null)
                        _coinValueChangeAction.Invoke(_coinValue);
                

            
            get  return _coinValue; 
        


        private int _level;
        public int Level
        
            set
            
                if (_level != value)
                
                    _level = value;
                    if (_levelValueChangeAction != null)
                        _levelValueChangeAction.Invoke(_level);
                

            
            get  return _level; 
        
接下来我们继续添加对应的属性的时候会发现大量的代码在重复出现,所以我们得写一个基类来将相同的进行封装,将其定义为:

using System;



namespace Assets.Scripts

    public class ReactiveProperty<T>
    

        private T _value;

        public T Value
        
            set
            
                if (!_value.Equals(value))
                
                    _value = value;
                    _observer.Invoke(_value);
                
             
            get  return _value; 
        

        private Action<T> _observer;

        public void Subscribe(Action<T> observer)
        
            _observer = observer;
        

       
    

用泛型进行重构一下,就可以满足所有类型,里面就只有一个方法,ReactiveProperty作为一个被观察的对象,让它可以订阅一个观察者,我们将这个被观察的对象称为一条数据流,怎么理解这个流呢,就是在整个游戏中这个值发生变化的次数可能不止一次,这个流上简单的记录整个游戏中这个数据何时发生变化的。当这个数据流发生了数据变更的时候才对其监听该数据流的监听者执行相应的监听操作,所以模型层的代码就可以变得非常简单了。这里如果我们一个数据流有多个监听对象该怎么办呢?(大家可以自己脑补一下,到具体的响应式编程我会写出来)

    public class Model
    

        public ReactiveProperty<int> CoinValue  set;get; 

        public Model()
        
            CoinValue = new ReactiveProperty<int>();
        

    
那么控制层的代码就同样也可以非常简单,控制层就只有对数据的简单的操作。

    public class Controller
    

        private Model _model;
        public Controller(Model model)
        
            _model = model;
        


        public void AddCoin(int count)
        
            _model.CoinValue.Value += count;
          
        

        public void ConsumCoin(int count)
        
            _model.CoinValue.Value -= count;
        

            
    
控制层就只有一些数据的加减这些基本的操作了。视图层的代码就没有任何的改动了,我们始终遵循一个原则就是我们最后操作的是实体类,而UI层不应该有任何关于数据操作的逻辑,它应该只是显示功能和交互功能,例如购买一件商品我们点击购买按钮,我们金币发生了变化,UI购买按钮点击只是去通知我们的模型层的数据发生变化,然后触发了我们UI上的CoinText发生了变化,而不是点击购买按钮然后马上修改CoinText显示,然后再去通知数据模型层发生变化,如果这其中出现了bug的话,我们肯定会认为是模型层的逻辑出现了问题,而不应该考虑UI显示的问题。还有例如我们捡起一把斧头,我们在UI背包中可以清楚的看到确实多了一把斧头,这个过程的原理同样是背包模型中添加了一个道具才触发UI背包中多了一件斧头。但是如果我们需要丢弃这样一把斧头的时候这个时候我们是应该先让背包模型中的数据发生变化后触发UI背包的显示呢。其实这里我们完全没必要按照上面的哪一种做法来处理,我们可以先马上处理UI背包显示移除然后通知模型层数据发生变化。面对2中不同的情况我们出现了2种不同的做法。为什么呢?后面一种如果我们先通知模型层数据改变最后其实又回到UI背包层中将斧头背包想移除。这其中调用关系就是我们先从UI层转到模型层,再从模型层转到UI层。如果我们把移除斧头在移除按钮按下的时候直接对其UI进行移除操作,同时又让模型层的数据发生变更,这样岂不是更加方便么?但是面对第一种情况这样做就非常不合理了,为什么呢?你无法知道一个text显示的数字还是其他的什么东西,你又如何对其进行修改呢?还有原始的数据是多少你又无法得知(难道直接对Text中的字符串进行解读?这不是扯淡么)。而后面这种情况就不一样。这就是用一种基本的响应式思想去解读MVC模式。



这张图片还是在wiki上找的,之前国外一个大学公开课里面一个讲MVC很好的一个例子图片没找到,先用这样一张图片暂代一下,找到之后替换掉。这张图就很好的解释了MVC这3个层应该是怎样的一种关系。这就是简单响应式编程思想在MVC上的应用,接下来讲一下MVVM模式中的响应式编程。之前写过一篇MVVM框架,一般这种ui框架应用于一些比较大的游戏,例如一个游戏中存在15个以上的弹出框就可以考虑使用这种方式,比较小的用mvc完全可以胜任,当然响应式编程也不止应用于UI上的,它的功能很大一方面是解决代码之间的耦合性即一个mono脚本运行需要持有另外几个或者多个类的应用,这种情况是必然,因为脚本之间需要进行数据交换,读取,修改等操作。而我们有时对游戏中数据的更改的时候又同时需要对它依赖的数据同时进行修改。当然如果整个游戏中进行一次修改这到无所谓,但是当游戏存在超过多次地方对这个数据进行修改的时候,你会发现你在写重复的代码做重复的工作,所以响应式编程的思想也是为了解决代码的冗余。下面我截取Databind插件部分源代码。首先截取它的VM层的部分代码。


它的VM层的代码基本全是这种格式得数据。

  public class Property : IDataProvider
    
        #region Fields

        /// <summary>
        ///   Current data value.
        /// </summary>
        private object value;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        ///   Constructor.
        /// </summary>
        /// <param name="value">Initial value.</param>
        public Property(object value)
        
            this.Value = value;
        

        /// <summary>
        ///   Constructor.
        /// </summary>
        public Property()
        
        

        #endregion

        #region Delegates

        #endregion

        #region Events

        /// <summary>
        ///   Called when the value of the property changed.
        /// </summary>
        public event ValueChangedDelegate ValueChanged ;

        #endregion

        #region Properties

        /// <summary>
        ///   Current data value.
        /// </summary>
        public object Value
        
            get
            
                return this.value;
            
            set
            
                var changed = !Equals(this.value, value);
                if (!changed)
                
                    return;
                
                
                this.value = value;

                this.OnValueChanged();
            
        

        #endregion

        #region Methods

        /// <summary>
        ///   Should be called after the data value changed.
        /// </summary>
        protected void OnValueChanged()
        
            var handler = this.ValueChanged;
            if (handler != null)
            
                handler();
            
        

        #endregion
    

    /// <summary>
    ///   Generic data property to monitor a data value.
    /// </summary>
    /// <typeparam name="T">Type of data.</typeparam>
    public sealed class Property<T> : Property
    
        #region Constructors and Destructors

        /// <summary>
        ///   Constructor.
        /// </summary>
        public Property()
        
        

        /// <summary>
        ///   Constructor.
        /// </summary>
        /// <param name="value">Initial value.</param>
        public Property(T value)
            : base(value)
        
            this.Value = value;
        

        #endregion

        #region Properties

        /// <summary>
        ///   Current data value.
        /// </summary>
        public new T Value
        
            get
            
                var value = base.Value;
                return value != null ? (T)value : default(T);
            
            set
            
                base.Value = value;
            
        

        #endregion
    
上面这个类是以上那个Property<string>这个类的基类,不难看出它这里最重要的就是一个Value属性和一个ValueChanged的事件,这里其实和我们这里写的ReactiveProperty<int> 基本就差不多了,这里比较得知MVVM和这里的MVC有什么区别呢?其实本质都是一样的,只不过MVVM使用编辑器将数据和UI之间的绑定关系确立了,而我们的MVC却并没有,它将2个层分离开来了,需要他们产生关系的时候在一个初始化的地方进行绑定即可。还有MVVM对大量的UI控件进行封装,使得程序员更加方便的开发,而这种简单的MVC并没有做这样一件事。2中UI模式只是对响应式编程思想一种简单得应用,这种思想其实早在网页上得到体现。













以上是关于没用的响应式编程?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin结合Spring WebFlux实现响应式编程

「R」Shiny:响应式编程server 函数

浅析Java响应式编程(Reactive Programming)

前几天有个同学问我,“什么是响应式编程”?另,它和函数式编程有啥区别?

响应式圣经:10W字,实现Spring响应式编程自由

赠书:响应式编程到底是什么?