zoukankan      html  css  js  c++  java
  • JS二维数据处理逻辑封装探究

    一、目的

    定义出一个专门用于处理二维数据的组件,所谓二维数据就是能用二维表格显示出来的数据,所谓处理就是增删改查,很简单。

    二、约束

    外部程序给该组件传入如下形式的对象,让该组件自行解析。

    var testData = {
        metadata: [{name: 'fid', label: 'fid', datatype: 'string', visible: 'false'},
            {name: 'fName', label: '名称', datatype: 'string', visible: 'true'},
            {name: 'fAge', label: '年龄', datatype: 'int', visible: 'true'}],
        data: [{fid: 'id_1', fName: 'yi', fAge: 25},
            {fid: 'id_2', fName: 'anna', fAge: 24},
            {fid: 'id_3', fName: 'kate', fAge: 26}]
    };

    testData分为metadata和data两部分,两部分均为数组。

    metadata内的每一个元素可以对应于表格的一列,持有列标识(键名暂默认为name),列标签,数据类型,是否可见等属性,并不仅限于传入以上属性,外部程序想传入什么就传入什么,组件只管解析和缓存,不管功能。

    data内每一个元素的键集合就是所有metadata的列标识,值就是对应的数据。

    可以用表格把testData展现为如下外观,其中列明对应于metadata的列标签值(label)

    fid 名称 年龄
    id_1 yi 25
    id_2 anna 24
    id_3 kate 26

     

     

     

     

    三、实现思路

    组件命名为DataModel,仅有一个实例,获取方式为DataModel.getInstance。并提供以下公共方法:

    parse: 解析数据

    size: 获取数据数量

    getData(index, prop): 根据索引和属性获取数据值

    getMetadata(key, prop): 根据键和属性获取元数据属性值

    foreach(type, fn): 迭代数据或元数据。其中type仅限于'metadata'和'data',迭代过程中会给fn传入迭代对象

    update(index, prop, newValue[, callback, url]): 更新数据,返回布尔值。其中callback会在更新成功后调用,url是服务端更新数据地址

    addView(view): 添加视图,view必须定义render方法,DataModel更新后(update或parse执行)会触发调用,传入更新信息,通知视图更新

    四、单元测试

    这里采用TDD的方式,先写测试用例,再写实现。单元测试用例可以在一定程度上减轻手工测试的负担,快速验证基本功能的正确性。并且用例覆盖的场景越全面,质量的有力保障手段,有了它,对代码的修改不再像过去那般战战兢兢了。

    1、场景设置

    parse
    预处理:插入预设数据

    size

    场景1:获取数据size
    getData
    场景2:根据索引和属性获取数据值
    getMetadata
    场景3:根据键和属性获取元数据属性值
    foreach
    场景4:迭代元数据
    场景5:迭代数据
    update
    场景6:给字符串类型的字段更新数据
    场景7:给int类型的字段更新字符串数据
    场景8:给int类型的字段更新int数据
    场景9:更新一个等于旧值的数据
    addView
    场景10:更新数据,所有注册于DataModel上的视图都会得到通知

    2、qunit测试框架

    这是jQuery团队所使用的单元测试框架,轻量级,入门简单,基本用法可以参阅cookbook,这里不作详细介绍。

    测试DataModel的代码如下所示:

      1 /**
      2  * 测试DataModel
      3  */
      4  
      5 // 预处理:插入预设数据
      6 var testData = {
      7     metadata: [{name: 'fid', label: 'fid', datatype: 'string', visible: 'false'},
      8         {name: 'fName', label: '名称', datatype: 'string', visible: 'true'},
      9         {name: 'fAge', label: '年龄', datatype: 'int', visible: 'true'}],
     10     data: [{fid: 'id_1', fName: 'yi', fAge: 25},
     11         {fid: 'id_2', fName: 'anna', fAge: 24},
     12         {fid: 'id_3', fName: 'kate', fAge: 26}]
     13 };
     14 
     15 // 获取DataModel实例
     16 var model = DataModel.getInstance();
     17 model.parse(testData);
     18 
     19 // 测试parse效果
     20 // -场景1:获取数据size
     21 test('data size', function(){
     22 var size = model.size();
     23 equal(size, 3, 'get data size');
     24 });
     25 
     26 // -场景2:根据索引和属性获取数据值
     27 test('get data value by index and prop', function(){
     28 var data_1 = model.getData(1, 'fName'),
     29     data_2 = model.getData(2, 'fAge');
     30     strictEqual(data_1, 'anna', 'OK, name is ' + data_1);
     31     strictEqual(data_2, 26, 'OK, age is ' + data_2);
     32 });
     33 
     34 // -场景3:根据键和属性获取元数据属性值
     35 test('get metadata value by key and prop', function(){
     36 var val_1 = model.getMetadata('fid', 'visible'),
     37     val_2 = model.getMetadata('fName', 'label'),
     38     val_3 = model.getMetadata('fAge', 'datatype');
     39     strictEqual(val_1, false, 'OK, visible is ' + val_1);
     40     strictEqual(val_2, '名称', 'OK, label is ' + val_2);
     41     strictEqual(val_3, 'int', 'OK, datatype is ' + val_3);
     42 });
     43 // -测试foreach
     44 test('test foreach', 7, function(){
     45     var i = 0;
     46     // 场景4:迭代元数据
     47     model.foreach('metadata', function(obj){
     48         var result = {name: obj.name, label: obj.label, datatype: obj.datatype, visible: obj.visible};
     49         deepEqual(result, testData.metadata[i], 'for each one metadata testing');
     50         i++;
     51     });
     52     // 场景5:迭代数据
     53     i = 0;
     54     model.foreach('data', function(obj){
     55         deepEqual(obj, testData.data[i], 'for each one data testing');
     56         i++;
     57     });
     58     try{
     59         model.foreach('other', function(obj){
     60             ok(false, "shouldn't be called");
     61         });
     62     }catch(e){
     63         ok(true, 'should reach here');
     64     }
     65 });
     66 
     67 // 写入数据
     68 // -更新数据, 预期断言数是8,更新失败后回调不应该被调用,里面的断言也不会被执行。
     69 test('update data by index, prop and new value', 9, function(){
     70     var newValue = 'yi_2', newValue_int = 30;
     71     // 场景6:给字符串类型的字段更新数据
     72     var result = model.update(0, 'fName', newValue);
     73     strictEqual(result, true, 'update string value success');
     74     // 场景7:给int类型的字段更新字符串数据
     75     result = model.update(1, 'fAge', newValue, function(obj){
     76         ok(false, "callback function shouldn't be called if update failed");
     77     });
     78     strictEqual(result, false, 'OK, update string value into int field failed');
     79     // 更新失败,保留的还是旧值
     80     var data = model.getData(1, 'fAge');
     81     strictEqual(data, 24, 'ok, value is not changed if update failed');
     82     // 场景8:给int类型的字段更新int数据
     83     result = model.update(1, 'fAge', newValue_int, function(obj){
     84         strictEqual(obj.index, 1, 'in calllback obj index is right');
     85         strictEqual(obj.newValue, 30, 'in callback obj newValue is right');
     86         strictEqual(obj.prop, 'fAge', 'in callback obj prop is right');
     87     });
     88     strictEqual(result, true, 'OK, update int value into int field success');
     89     // 更新成功,保留的是新值
     90     data = model.getData(1, 'fAge');
     91     strictEqual(data, 30, 'ok, value is changed if update success');
     92     // 场景9:更新一个等于旧值的数据
     93     result = model.update(1, 'fAge', 30, function(obj){
     94         ok(false, "this shouldn't be called if newValue is equal to oldValue");
     95     });
     96     strictEqual(result, true, "update should return true if old value is equals to new value");
     97     
     98 });
     99 // -更新数据,检验渲染视图
    100 test('render data after update data model', 8, function(){
    101     // 场景10:更新数据,所有注册于DataModel上的视图都会得到通知
    102     var view1 = {render: function(obj){checkRender(obj);}},
    103         view2 = {render: function(obj){checkRender(obj);}};
    104     model.addView(view1).addView(view2);
    105     model.update(1, 'fAge', 28);
    106     function checkRender(obj){
    107         strictEqual(obj.id, 'id_2', 'check id');
    108         strictEqual(obj.index, 1, 'check index');
    109         strictEqual(obj.prop, 'fAge', 'check prop');
    110         strictEqual(obj.newValue, 28, 'check newValue');
    111     }
    112 });
    View Code

    严格来说,流程走到这里,因为还没有写实现,应该会报找不到符号之类的错才对。

    还有一点要提一下,可以看出,把每个测试用例展开后显示的绿色字,就是给每个断言方法传入的第三个字符串参数。这个字符串更好的写法也许应该是对应于刚才列出的场景。这里没有改,我也是写到这里才想到这一点的。另外我也是第一次用这个东西,所以应该还会有更好的规范写法是我没有发现的,这点请大家自行思考。

    五、具体实现

    这里贴出DataModel的代码,还有很多功能点没有实现,例如删除,校验,还有parse的处理方式实在太蹩脚了,data只是单纯的赋值,按照数据类型校验或转换啥的都没有。这里的目的更多的是为了表达一种思想。另外这里贴出的肯定也不是最好的方式,因为本人也是缺少经验的js初学者,写的过程中经常发现这里不对,那种处理方式更好一些,大大小小的重构是经常的。这里之所以要写测试用例也是因为这个原因,管我怎么摆弄这些代码,写好了就run一下测试,只要公共接口能正确跑对就ok。

      1 var DataModel = (function(){
      2 // 惰性加载
      3 var _instance;
      4 return {
      5     getInstance: function(){
      6         if(!_instance)
      7             _instance = constructor();
      8         return _instance;
      9     }
     10 };
     11 function constructor(){
     12     var metadata, data,
     13         viewList = [];
     14     function renderView(obj){
     15         for(var i=0, len=viewList.length; i<len; i++){
     16             viewList[i].render(obj);
     17         }
     18     }
     19     return {
     20         parse: function(obj){
     21             if(obj && obj.metadata){
     22                 metadata = {};
     23                 var meta, name, i, len;
     24                 for(i=0, len=obj.metadata.length; i<len; i++){
     25                     meta = obj.metadata[i];
     26                     name = meta.name;
     27                     metadata[name] = new Metadata(meta);
     28                 }
     29                 // 注意:data是可以被清空的
     30                 data = obj.data;
     31                 // 给注册的视图发送消息
     32                 renderView();
     33             }else{
     34                 throw new Error('the param is invalid');
     35             }
     36         },
     37         // 从服务端取数据
     38         fetchDataFromServer: function(url){
     39             var self = this;
     40             yi.xhr.request('POST', url, {
     41                 success: function(responseText){
     42                     if(responseText){
     43                         var obj = eval("(" + responseText + ")");
     44                         self.parse(obj);
     45                     }
     46                 }
     47             });
     48         },
     49         size: function(){
     50             if(data)
     51                 return data.length;
     52             else
     53                 throw new Error('data is null');
     54         },
     55         getData: function(index, prop){
     56             if(data)
     57                 return data[index][prop];
     58             else
     59                 throw new Error('data is null');
     60         },
     61         getMetadata: function(key, prop){
     62             if(metadata)
     63                 if(metadata[key])
     64                     if(typeof metadata[key][prop] == 'boolean' || metadata[key][prop]){
     65                         var val = metadata[key][prop];
     66                         if(val == 'true' )
     67                             return true;
     68                         else if(val == 'false')
     69                             return false;
     70                         else
     71                             return val;
     72                     }
     73                     throw new Error('metadata[' + key + '] has not the prop: ' + prop);
     74                 throw new Error('metadata has not the key: ' + key);
     75             throw new Error('metadata is null');
     76         },
     77         update: function(index, prop, newValue, callback, url){
     78             // 新旧值如果相等就直接返回true可以了
     79             var oldValue = data[index][prop];
     80             if(oldValue === newValue)
     81                 return true;
     82             var datatype = this.getMetadata(prop, 'datatype');
     83             // 根据数据类型校验
     84             switch(datatype){
     85                 case 'int':
     86                     if(isNaN(parseInt(newValue)) || parseFloat(newValue) != parseInt(newValue)){
     87                         return false;
     88                     }
     89                     newValue = parseInt(newValue);
     90                     break;
     91                 case 'float':
     92                 case 'number':
     93                     if(isNaN(newValue = parseFloat(newValue))){
     94                         return false;
     95                     }
     96                     break;
     97             }
     98             // 更新数据
     99             data[index][prop] = newValue;
    100             // 通知服务端更新数据
    101             if(url){
    102                 var id = data[index]['fid'],
    103                     req = 'id=' + id + '&key=' + prop + '&value=' + newValue,
    104                     self = this;
    105                 yi.xhr.request('post', url, {
    106                     failure: function(){return false;}
    107                 }, req);
    108             }
    109             var obj = {'id': data[index]['fid'], 'index': index, 'prop': prop, 'newValue': newValue};
    110             // 给注册的视图发送消息
    111             renderView(obj);
    112             // 更新后回调
    113             if(callback) callback(obj);
    114             return true;
    115         },
    116         // 遍历metadata或者data
    117         foreach: function(type, fn){
    118             if(!fn) throw new Error('param[fn] is required.');
    119             if(type === 'metadata'){
    120                 for(var key in metadata){
    121                     fn(metadata[key]);
    122                 }
    123             }else if(type === 'data'){
    124                 for(var i=0, len=data.length; i<len; i++){
    125                     fn(data[i]);
    126                 }
    127             }else{
    128                 throw new Error('param[type] is only limit in "metadata" and "data".');
    129             }
    130         },
    131         // 添加视图
    132         addView: function(view){
    133             for(var i=0, len=viewList.length; i<len; i++){
    134                 if(view == viewList[i])
    135                     return this;
    136             }
    137             viewList.push(view);
    138             return this;
    139         }
    140     };
    141 }
    142 })();
    143 
    144 // 元数据数据结构
    145 function Metadata(config){
    146     for(var prop in config){
    147         var val = config[prop];
    148         this[prop] = val;
    149     }
    150     // name属性必须指定
    151     if(!this['name'])
    152         throw new Error('name must be provided!');
    153 }
    154 Metadata.prototype = {
    155     constructor: Metadata,
    156     // 默认属性
    157     label: 'Default Label',
    158     datatype: 'string',
    159     visible: true
    160 };
    View Code

    简单说明下这段代码的结构:

    1、新建DataModel变量

    首字母大写,因为它是个类。该变量的值由以下的结构返回

    var DataModel = (function(){
    // 惰性加载
    var _instance;
    function constructor(){}
    return {
        getInstance: function(){
            if(!_instance)
                _instance = constructor();
            return _instance;
        }
    };
    })();

    =号右边的是一个定义后立刻执行的函数,起到闭包的作用(外部不能访问私有成员_instance和constructor;getInstance方法可以访问_instance和constructor,外部可以访问getInstance;这两个私有成员值不会消失)。

    getInstance里面是经典的单例模式写法。由于constructor有可能是一个比较耗费资源的操作,因此不必在页面加载的时候立刻执行,延迟到用户需要的时候,再通过getInstance获取。

    2、另一个闭包,constructor函数

    function constructor(){
        var metadata, data,
            viewList = [];
        function renderView(obj){
            for(var i=0, len=viewList.length; i<len; i++){
                viewList[i].render(obj);
            }
        }
        return{
            // parse
            // fetchDataFromServer
            // size
            // getData
            // ...
        };
    }

    外部程序通过getInstance获得constructor返回的那个对象字面量,然后就可以访问组件的公共api(parse,getData,etc.)了。

    可以通过addView给私有成员viewList添加成员。update或parse执行到最后会内部调用私有方法renderView,给所有注册的视图发送消息(观察者模式)。

    注意,注册到DataModel的视图必须实现render方法。

    3、另外,这个实现代码存在两个约束。

    一是在update方法里面,通知服务端更新数据之后,构造更新信息obj那里,其中id值是取data[index]['fid']。也就是说,数据的主键名已经写死为fid了。这里我能够想到的就只有两种处理方式,一种就是现在所使用的,一种是生成传递给parse方法的参数obj(自己写或者服务端生成)时,指定一个元数据对象为主键(可以给主键元数据对象增加一个identity或primary属性)。经过考虑,我还是觉得用写死的处理方式比较好。二是元数据里面,列标识的键名也写死为name了,而且在构造Metadata对象时,参数必须指定name属性值。

  • 相关阅读:
    StarRTC , AndroidThings , 树莓派小车,公网环境,视频遥控(二)小车端
    StarRTC , AndroidThings , 树莓派小车,公网环境,视频遥控(一)准备工作
    公司名称后缀 Inc. Co.,Ltd.
    Linux C定时器使用指南
    配置QQ企业邮箱小结
    常用PHP文件操作函数
    Git基本命令 -- 基本工作流程 + 文件相关操作
    Git基本命令 -- 创建Git项目
    Entity Framework Core 2.0 入门
    用VSCode开发一个asp.net core2.0+angular5项目(5): Angular5+asp.net core 2.0 web api文件上传
  • 原文地址:https://www.cnblogs.com/var-iable/p/3176576.html
Copyright © 2011-2022 走看看