为什么你需要ViewObject

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,164评论 1 23
  • D君说“人做到两点就可以保证心理健康:接受现实,与时俱进。” 我做到了吗?好像没有…这是让人有些感伤难捱的。 确实...
    灵子94阅读 262评论 7 0
  • 今天孩子被点名作业没有写完,原因是没有发给他卷子,我想了想,孩子二年级了,我还没有数学老师的电话,我不能和老师...
    67fbaec5208f阅读 282评论 0 0