为什么你需要ViewObject

0.068字数 2187阅读 3294
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

推荐阅读更多精彩内容