开发者

java开发技巧代码写的快且bug少的原因分析

开发者 https://www.devze.com 2022-12-13 10:34 出处:网络 作者: leobert-lan
目录前言最常见的客户端bug为什么很浅显的问题没有被发现可能存在的接缝创造假数据再次审视如何处理接缝基于Retrofit的CallAdapter基于Retrofit的Interceptor其他针对Okhttp的封装写在最后前言
目录
  • 前言
  • 最常见的客户端bug
  • 为什么很浅显的问题没有被发现
  • 可能存在的接缝
  • 创造假数据
  • 再次审视如何处理接缝
    • 基于Retrofit的CallAdapter
    • 基于Retrofit的Interceptor
    • 其他针对Okhttp的封装
  • 写在最后

    前言

    读者诸君,今日我们适当放松一下,不钻研枯燥的知识和源码,分享一套高效的摸鱼绝活。

    我有一位程序员朋友,当时在一个团队中开发android应用,历经多次考核后发现:

    在组内以及与IOS团队的对比中:

    • 他的任务量略多
    • 但他的bug数量和严重度均低
    • 但他加班的时间又少于其他人

    不禁令人产生好奇,他是如何做到代码别的又快,质量又高的

    经过多次研究我终于发现了奥秘。

    为了行文方便我用"老L"来代指这位朋友。

    最常见的客户端bug

    • "老L,听说昨晚python上线,你又坐那摸鱼看测试薅别人,有什么秘诀吗?"
    • 老L:"秘诀?倒也谈不上,你这么说,我倒是有个问题,你觉得平日里最常见的bug有哪些?"
    • "emm,编码上不健壮的地方,例如NPE,IndexOutOfBoundsException,UI上的可就海了去了,文本长度不一导致显示不下,间距问题,乱七八糟的一大堆"
    • 老L:"哈哈,都是些看起来很幼稚、愚蠢的问题吧?是不是测试挂嘴边的那句:' 你就不能跑一跑吗,你又不瞎,跑两下不就看到了,这么明显!!!' "
    • 我突然来了兴致,"你是说我们有必要上 TDD(test-driven-develop),按照DevOps思想,在CI(Continuous Integration)的时候,顺带跑自动化测试用例发现问题?"
    • 老L突然打断了我:"不要拽你那些词了,记住了,事情是要人干的,机器只能替代可重复劳动,现在还不能替代人的主观能动性,拽词并不能解决问题。我们已经找到了第一个问题的答案,现在换个角度"

    平日里最常见的bug有哪些?

    • 编码不健壮, 例如NPE,IndexOutOfBoundsException
    • UI细节问题, 例如文本长度不一导致显示不下,间距,等

    为什么很浅显的问题没有被发现

    老L:"那么问题来了,为什么这些浅显的问题,在交测前没有被发现呢?"

    我陷入了思考...

    是开发们都很懒吗?也不至于啊!

    是时间很紧来不及吗?确实节奏紧张,但也不至于不给调试就拿去测了!

    "emm, 可能是迭代的节奏的太频繁,压力较大,并没有整块的时间用来自测联调"

    老L接过话茬,"假定你说的是正确的,那么就有两种可能。"

    "第一种,自测与联调要比开发还要耗费心思的一件事情。但实际上,你我都知道,这一点并站不住脚!"

    "而第二种,就是在开发阶段无法及时测试,拖到开发完,简单测测甚至被催促着就交差了"

    仔细的思考后

    • 业务逐步展开,无法在任意时间自由地进行有效的集成测试
    • 后端节奏并不比前端快多少,在前端的开发阶段,难以借助后端接口测试,也许接口也有问题

    "确实,这是一个挺麻烦的问题,听你一说,我感觉除了多给几天,开发完集中自测一波才行" 我如是说到。

    "NO NO NO",老L又打断了我:"你想的过多了,你想借助一个可靠的、已经完备的后端系统来进行自测。对于你的需求来说,这个要求过高了,你这是准备干QA的活"

    "我帮你列举一下情况"

    • 一些数据处理的算法,这种没有办法,老老实实写单元测试,在开发阶段就可以做好,保障可靠性
    • UI呢,我们现在写的代码,基本都做到了UI与逻辑分层,只要能模拟数据,就能跑起来看页面
    • 业务层,后端逻辑我们无法控制,但 Web-API 调用的情况可以分析下并做一下测试,而对于返回数据的jsON结构校验、约束性校验也可以考虑做一下测试

    总而言之,我们只需要先排除掉浅显的错误。而这些浅显的错误,属于情况2、3

    老L接着说道:"你先歇歇吧,我来说,你再插嘴这文章就太长了!"

    接下来就可以实现矛盾转移:"如何模拟数据进行测试",准确的说,问题分成两个子问题:

    • 如何生成模拟数据
    • 如何从接缝中塞入数据,让系统得以使用

    可能存在的接缝

    先看问题2:"如何从接缝中塞入数据,让系统得以使用"

    脑暴一下,可以得出结论:

    应用内部

    • 替换调用web-api的业务模块,使用假数据调用业务链,一般替换Presenter、Controller实例
    • 替换Model层,不调用web-api,返回假数据或用假数据调用回调链
    • 侵入网络层实现,不进行实际网络层交互,直接使用假数据
    • 遵循切面,向缓存等机制模块中植入假数据

    应用外部

    • 使用代理,返回假数据
    • 假数据服务器

    简单分析:

    • "假数据服务器" ,并且使用逻辑编造假数据的代价太大,过。
    • "使用代理,返回假数据",可以用于特定问题的调试,不适用广泛情况,过。
    • "替换调用web-api的业务模块",成本过大,过。
    • "替换Model层",对项目的依赖注入管理具有较大挑战,备选,可能带来很多冗余代码。
    • "侵入网络层实现",优选。
    • "向缓存等机制模块中植入假数据",操作真实的缓存较复杂,但可以考虑增加一个 Mock缓存实现模块,基于SPI等机制,可以解决冗余代码问题,备选。

    得出结论:

    • 方案1:"侵入网络层实现",优选
    • 方案2:"替换Model层",(项目的依赖注入做得很好时)作为备选,可能带来冗余代码
    • 方案3:"向缓存等机制模块中植入假数据",增加一个 Mock缓存实现模块,备选。(基于SPI等机制,可以解决冗余代码问python题)

    再仔细分析: 方案1和方案3可以合并,形成一个完整的方案,但未必需要限定在缓存机制中

    OK 我们先搁置一下这个问题,看前一个问题。

    创造假数据

    简单脑暴一下,无非三种:

    人工介入,手动编写 -- 成本过大

    • 可能在前期准备好,基本是纯文本
    • 可能使用一个交互工具,在需要数据时介入,通过图形化操作和输入产生数据

    人工介入,逻辑编码

    • 基于反射等自省机制,并完全随机或者基于限制生成数据

    "第一种代价过大,暂且抛弃"

    "第二种可以采用,但是人力成本不容忽视! 一个可以说服我使用它的理由是:"可以精心设计单测数据,针对性的发现问题"

    "第三种很轻松,例如使用Mockito,但生成合适的数据需要花费一定的精力"

    我们来扒一扒第三种方式,其核心思想为:

    获取类信息,得到属性集

    遍历属性填充 >

    • 基础类型、箱体类型,枚举,确定取值范围,使用Random取值,赋值
    • 普通类、泛型类,创建实例,回归步骤1
    • 集合、数组等,创建实例,回归步骤1,收集填充

    不难得出结论,这一方法虽然很强大,但 创建高度定制化的数据 是一件有挑战的事情。

    举个例子,模拟字符串时,一般会使用语料集作为枚举,进行取值。要得到“地址”、“邮箱”等特定风格的数据,需要结合框架做配置,客观上存在较高地学习、使用门槛。

    你也知道,前几年我图好玩,写了个 mopythonck库 。

    必须强调的一点:“我并不认为我写的库比Mockito等库强大,仅仅是在我们开发人员够用的基础上,做到尽可能简单!”

    所以,我能使用它便捷的生成合适的假数据,在开发阶段及时的进行 “伪集成”

    此刻,我再也忍不住要发言了:“且慢,老L,你这个做法有一定的侵入性吧。而且,如果数据类在不同业务下复用的话,是否存在问题呢?”

    老L顿了顿,“确实,google的annotations是源码级注解,并不是运行时,我为了保持简单,使用了运行时反射而非代码生成。所以确实存在一定的代码侵入性”。

    但是,我们可以基于此建立一套简单的MOCK-API,这样就不存在代码侵入了。

    另外,也可以增加一套Annotation-Processor 实现方案,这样就可以适当沿用项目中的注解约束了,但我个人认为华而不实。

    看你的第二个问题,Mocker一开始确实存在这个问题,有一次从Spring的JSR380中得到灵感,我优化了注解规则,这个问题已经被解决了。得空你可以顺着这个图看看:

    java开发技巧代码写的快且bug少的原因分析

    或者去看看代码和使用说明:github.com/leobert-lan…

    再次审视如何处理接缝

    此时我已经有点云里雾里,虽然听起来很牛,如何用起来呢?我还是很茫然,简直人麻了!不得不再次请教。

    老L笑着说:“你问的是一个实践方案的问题,而这类问题没有银弹.不同的项目、不同的习惯都有最适宜的方法,我只能分享一下我的想法和做法,仅做参考”

    在之前的项目中,我自己建了一个Mock-API,利用我的Mocker库,写一个假数据接口就是分分钟的事情。

    测试机挂上charles代理,有需要的接口直接进行mapping,所以在客户端代码中,你看不到我做了啥。

    当然,这个做法是在软件外部。

    如果要在软件内部做,我个人认为这也是一个华而不实的事情。不过不得不承认是一件好玩的事情,开发者_JAVA那就提一些思路。

    基于Retrofit的CallAdapter

    public interface CallAdapter<R, T> {
        Type responseType();
        T adapt(Call<R> call);
        abstract class Factory {
            public abstract @Nullable
            CallAdapter<?, ?> get(Type returnType, Annotation[] annotations,
                                  Retrofit retrofit);
            protected static Type getParameterUpperBound(int index, 
                                                         ParameterizedType type) {
                return Utils.getParameterUpperBound(index, type);
            }
            protected static Class<?> www.devze.comgetRawType(Type type) {
                return Utils.getRawType(type);
            }
        }
    }
    

    很明显,我们可以追加注解,用以区分是否需要考虑mock;

    可选:对于有可能需要mock的接口,可以继续追加切面,实现在软件外部控制使用 mock数据 或 真实数据

    而Retrofit已经使用反射确定了方法的 return Type ,在Mocker中也有适应的API直接生成假数据

    基于Retrofit的Interceptor

    相比于上一种,拦截器已经在Retrofit处理流程中靠后,此时在 Chain 中能够得到的内容已经属于Okhttp库的范畴。

    所以需要一定的前置措施用于确定 "return Type"、"是否需要Mock" 等信息。可以借助Tag机制:

    @Documented
    @Target(PARAMETER)
    @Retention(RUNTIME)
    public @interface Tag {
    }
    @GET("/")
    Call&l编程t;ResponseBody> foo(@Tag String tag);
    

    最终从 Request#tag(type: Class<out T>): T? 方式获取,并接入mock,并生成 Response

    其他针对Okhttp的封装

    思路基本类似,不再展开。

    写在最后

    听完老L的思路,我若有所思,若有所悟。他的方案似乎很有效,而且直觉告诉我,这些方案中还有很多留白空间,例如:

    • 借用SPI等技术思路,可以轻易的解决 "Mock 模块集成与移除" 的问题
    • 提前外部控制是否Mock的接缝,可以在加一个工具APP、或者Socket+网页端工具 用以实现控制

    但我似乎遗漏了问题的开始

    是否原意做 用于约束假数据生成规则的基础建设工作呢??? 例如维护注解

    事情终究是人干的,人原意做,办法总比困难多。

    以上就是Java开发中代码写的快且bug少的原因分析的详细内容,更多关于java开发代码快bug少的资料请关注我们其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号