为什么你需要ViewObject

作者: diygreen | 来源:发表于2016-04-12 22:13 被阅读2682次
    WhyNeedViewObject.png

    作者:李旺成###

    时间:2016年4月12日###


    这里使用了一个解析当前天气 JSON 字符串得到原始 Model 后,将该 Model 的数据展示到一个简单的页面上来进行演示。

    先看下 Demo 的效果图:


    天气展示 Demo

    我理解的 VO

    VOViewObjectViewModel。关于它的解释在 Android MVP 详解(下)中,我做过简要的阐述。这里,再说说我是怎么理解 VO 的。

    VO,就是一切给 View 提供数据的对象。这个定义就很广泛了,所以我对 VO 做了如下的分类(下面会细说)。

    VO 的实现方式

    既然,所有给 View 提供数据的对象都可以称之为 VO,那么 VO 的来源或者说形式就很多了。我在这里根据 VO 的实现方式进行了分类,仅仅是一家之言,有疏漏之处,见谅。

    1. 单独的 VO 类

    Android MVP 详解(下)中建议专门建一个包 vo,用来存放该模块下的所有 VO 类。对于这一类,那就属于单独的 VO 类,或者更准确的说明是“独立的 VO 类”。

    要使用这种类型的 VO,有一个问题,它是独立的类,那么就需要另外的对象给它提供数据。在这里我认为提供(传递)数据的方式,大致有如下两种:

    A. 使用转换器

    专门使用一个转换器类,来做原始 Model 到 VO 的转换。如示例项目中的 VOConverterUtil.java 类。(在这类里偷了个懒,直接调用了“构造方法中转换”的方式进行了转换)
    还是看下代码吧:

    public class VOConverterUtil {
        public static WeatherVO getWeatherVOFromWeatherBean(WeatherBean weatherBean) {
            // 这里偷个懒
            WeatherVO weatherVO = new WeatherVO(weatherBean);
            return weatherVO;
        }
    }
    

    B. 构造方法中转换

    这个很好理解,就是在构造方法中进行数据转换。代码很简单,直接看代码:

    public WeatherVO(WeatherBean weatherBean) {
        if (weatherBean == null) return;
        isSuccess = "ok".equals(weatherBean.getStatus());
        int condCode = Integer.parseInt(weatherBean.getNow().getCond().getCode());
        String condCodeColorStr = "";
        if (condCode < 0) {
            weatherInfoIcon = R.mipmap.ic_snow;
            condCodeColorStr = "#000066";
        } else if (condCode < 60) {
            weatherInfoIcon = R.mipmap.ic_rain;
            condCodeColorStr = "#009900";
        } else if (condCode < 90) {
            weatherInfoIcon = R.mipmap.ic_cloudy;
            condCodeColorStr = "#993300";
        } else {
            weatherInfoIcon = R.mipmap.ic_sunshine;
            condCodeColorStr = "#cccc00";
        }
        weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+weatherBean.getNow().getCond().getTxt()+"</font>");
        relativeHumidity = "相对湿度:" + weatherBean.getNow().getHum();
        int tmpInt = Integer.parseInt(weatherBean.getNow().getTmp());
        if (tmpInt < 15) {
            temperatureIcon = R.mipmap.ic_lowtemperature;
        } else if (tmpInt < 33) {
            temperatureIcon = R.mipmap.ic_thermophilic;
        } else {
            temperatureIcon = R.mipmap.ic_hightemperature;
        }
        airPressure = "气压:" + weatherBean.getNow().getPres();
        precipitation = "降水量:" + weatherBean.getNow().getPcpn();
        visibility = "能见度:" + weatherBean.getNow().getVis() + " KM";
        windDirectionAngle = "风向角度:" + weatherBean.getNow().getWind().getDeg();
        windDirection = "风向:" + weatherBean.getNow().getWind().getDir();
        windPower = "风力:" + weatherBean.getNow().getWind().getSc();
        windSpeed = "风速" + weatherBean.getNow().getWind().getSpd();
    }
    

    2. 实现接口成 VO

    抽出单独的类,那么就多了一个类, Modle 如果很多的话,那不可避免 VO 的数量也会增加。有些人可能觉得没必要,这增加了项目复杂度(哈哈,任何的设计都有可能造成复杂度上升)。那么,这样,我们抽取一个接口,然后让原始 Model 去实现这个接口 —— 以后就可以“面向接口编程”了。

    思路很简单,那么直接上代码吧:
    抽取接口 IWeatherVO.java:

    public interface IWeatherVO {
    
        boolean isSuccess(); // "status": "ok", //接口状态
        int getWeatherInfoIcon(); // "code": "100", //天气状况代码 假设 <0 下雪, < 60 雨,大于 >60 < 90 阴, > 90 晴
        Spanned getWeatherInfoText(); // "txt": "晴" //天气状况描述 天气的文本描述
        String getRelativeHumidity(); //  "hum": "20%", //相对湿度(%)
        int getTemperatureIcon(); // "tmp": "32", //温度 温度图标
        String getAirPressure(); // "pres": "1001", //气压
        String getPrecipitation(); // 降水量
        String getVisibility(); // "vis": "10", //能见度(km)
        String getWindDirectionAngle(); // "deg": "10", //风向(360度)
        String getWindDirection(); // "dir": "北风", //风向
        String getWindPower(); // "sc": "3级", //风力
        String getWindSpeed(); // "spd": "15" //风速(kmph)
    
    }
    

    实现接口

    public class WeatherBean implements IWeatherVO {
    
        // 原始 Modle 中的字段都省略了,具体看源码吧
        ...
    
        //==========实现 VO 接口==========
        @Override
        public boolean isSuccess() {
            return "ok".equals(status);
        }
    
        @Override
        public int getWeatherInfoIcon() {
            int weatherInfoIcon;
            int condCode = Integer.parseInt(getNow().getCond().getCode());
            if (condCode < 0) {
                weatherInfoIcon = R.mipmap.ic_snow;
            } else if (condCode < 60) {
                weatherInfoIcon = R.mipmap.ic_rain;
            } else if (condCode < 90) {
                weatherInfoIcon = R.mipmap.ic_cloudy;
            } else {
                weatherInfoIcon = R.mipmap.ic_sunshine;
            }
            return weatherInfoIcon;
        }
    
        @Override
        public Spanned getWeatherInfoText() {
            Spanned weatherInfoText;
            int condCode = Integer.parseInt(getNow().getCond().getCode());
            String condCodeColorStr = "";
            if (condCode < 0) {
                condCodeColorStr = "#000066";
            } else if (condCode < 60) {
                condCodeColorStr = "#009900";
            } else if (condCode < 90) {
                condCodeColorStr = "#993300";
            } else {
                condCodeColorStr = "#cccc00";
            }
            weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+getNow().getCond().getTxt()+"</font>");
            return weatherInfoText;
        }
    
        @Override
        public String getRelativeHumidity() {
            return "相对湿度:" + getNow().getHum();
        }
    
        @Override
        public int getTemperatureIcon() {
            int temperatureIcon;
            int tmpInt = Integer.parseInt(getNow().getTmp());
            if (tmpInt < 15) {
                temperatureIcon = R.mipmap.ic_lowtemperature;
            } else if (tmpInt < 33) {
                temperatureIcon = R.mipmap.ic_thermophilic;
            } else {
                temperatureIcon = R.mipmap.ic_hightemperature;
            }
            return temperatureIcon;
        }
    
        @Override
        public String getAirPressure() {
            return "气压:" + getNow().getPres();
        }
    
        @Override
        public String getPrecipitation() {
            return "降水量:" + getNow().getPcpn();
        }
    
        @Override
        public String getVisibility() {
            return "能见度:" + getNow().getVis() + " KM";
        }
    
        @Override
        public String getWindDirectionAngle() {
            return "风向角度:" + getNow().getWind().getDeg();
        }
    
        @Override
        public String getWindDirection() {
            return "风向:" + getNow().getWind().getDir();
        }
    
        @Override
        public String getWindPower() {
            return "风力:" + getNow().getWind().getSc();
        }
    
        @Override
        public String getWindSpeed() {
            return "风速" + getNow().getWind().getSpd();
        }
    
    }
    

    3. 添加方法成 VO

    这个就更简单了,那就是连接口都不抽取了,直接提供上述接口中的方法。这里就不赘述了,思路是和上面提取接口一致,所提供的方法,目的就是方便在 View 中直接使用。(这个在 Android MVP 详解(下)中讨论过,略)

    4. 没有 VO

    没有 VO,那就是根本不使用 VO。如果你的项目是 MVP 的,那么就在 Presenter 中做数据转换的工作,然后提供给 View 展示。

    这对于很简单的 Model 和 简单的 View 是没有问题的,如果,Model 很复杂(字段很多,而且不能直接使用),那么 Presenter 的任务就会很重。

    这里就不做演示了,很多人应该都在这么用,或者曾经是这么用的。

    使用 VO 的好处

    上面说了一堆 VO 的实现方式,但是就是没提使用 VO 到底有何益处;或者说 VO 存在的意义。下面就我个人的理解,谈谈我认为 VO 的好处。

    统一命名习惯

    很多时候数据来源是网络(服务器端),那么这就可能有一个问题。服务器端的命名习惯可能与客户端有很大区别,还有不同服务器端开发的命名习惯也可能不同(如:使用 PHP 开发的服务器程序和使用 Java 开发的服务器程序命名很可能就是不同的)。

    简而言之,那就是服务器反给我们的字段和我们项目中的命名习惯不同,很多人说,这没办法啊!总不能让服务器改吧!

    是的,客户端和服务器端的命名很难统一,有人会说,不统一就不统一,又不影响使用。确实,不影响使用,但是,我们追求完美不是(先从最基本的命名规范做起,哈哈)。

    所以,从这个角度来考虑,我建议原始的 Model 那就按照接口文档来(当然,如果使用 Gson 的话,关于命名不统一还是可以解决的,有兴趣的可以自行 Google)。我们自己针对 View 定义一套 VO,这个可以完全按照我们自己的命名规范来,至少这里是统一的。

    解耦 View 和 Model

    解耦,这个就不用多说了吧!我都不直接使用你了,这还不是解耦,View 依赖的是 VO,而不再依赖原始的 Modle。关于解耦所带来的优点,这里就不详述了,一搜一堆...

    铺平数据结构

    铺平数据结构” —— 可以理解为将原来有多级(层级较深)的对象,转换为层级较浅的对象。

    我曾在项目中遇到这样一个问题:有很多相似的页面,但是服务器端给的字段都是不同的,这就需要建立多个 Model 来解析服务器给的数据。考虑到页面基本一样,那就不需要提供多个页面了,直接用一个页面,往里面填充不同的数据就可以了。那么,问题来了,这会导致要写很多重复的填充 View 的代码,因为 Model 是不同的。

    对于上述的问题,我的解决方案是,将页面中要使用的数据抽取为独立的 VO,该页面只需要从 VO 中获取数据即可。再就是,关于如何建立 Modle 去解析服务器数据的问题。这里,我只建了一个 Modle,将所有使用这个页面的接口中的返回字段都封装到一个 Modle 中。这得益于 Model 中多了字段,并不会影响 JSON 字符串到对象的转换(至少 Gson 是这样的)。

    上面说的这个例子,也可以认为是“铺平了数据结构”。

    在这个示例 Demo 中,可以很好的演示 —— “铺平数据结构” 。
    先看下原始的 JSON 字符串:

    {
        "status": "ok",
        "now": {
            "cond": {
                "code": "100",
                "txt": "晴"
            },
            "fl": "30",
            "hum": "20%",
            "pcpn": "0.0",
            "pres": "1001",
            "tmp": "32",
            "vis": "10",
            "wind": {
                "deg": "10", //风向(360度)
                "dir": "北风", //风向
                "sc": "3级", //风力
                "spd": "15" //风速(kmph)
            }
        }
    }
    

    看一下,上面的 JSON 字符串,如果需要获取风速,那么需要先访问 now,在访问 wind,然后才能获取到 spd 字段。在代码中就如下:

    weatherBean.getNow().getWind().getSpd();
    

    而在我们的 VO 中,可以直接取到:

    // 数据已经转换过了,这里直接可以取到
    public String getWindSpeed() {
        return windSpeed;
    }
    

    减少可能的问题

    其实,View 和 Model 的耦合就是一个很大的问题,哈哈,这个确实能解决。

    还有一些问题可以得到避免,例如,减少 View 中对 Model 的取值的各种判断(当然 MVP 就能解决),避免 Model 中的数据异常导致 View 崩溃。

    这里就不多说这个问题了,等你遇到的时候,自然就知道能够避免哪些问题了。(偷个懒,这个以后有机会再丰富吧)

    VO 使用演示

    直接看图吧,就不上 GIF 了。

    VO Class 演示 VO Interface 演示 VO Method 演示

    小结

    没有可以解决一切问题的妙药,no magic。

    关于上述 VO 的各种形式,需要根据具体的场景(项目)来区分,当然这也在很大程度上取决于个人的习惯以及项目的大小。

    如果是比较大的项目,那么建议直接抽出一个 VO 包来,为每个 View 都提供单独的 VO 对象,这样也可以保证项目的统一性,不会破坏层之间的依赖。

    如果是小项目,那么可以混着用,觉得哪种方式使用起来最方便,那就使用哪种吧!

    还是那句话,没有一定之规,要依据使用场景来确定。

    项目地址:
    GitHub

    相关文章

      网友评论

      本文标题:为什么你需要ViewObject

      本文链接:https://www.haomeiwen.com/subject/niqwlttx.html