Kotlin属性委托的巧妙使用-埋点上报封装

⏰ : 全文字数:3150+

🥅 : 内容关键字:kotlin, 属性委托,埋点设计

🤙 : 公_众_号:七郎的小院

🥅 : 更多文章:blog.softgeek.cn

概述

在实际的项目开发中,一定会有埋点上报的需求。可能每个项目上报的方式不一样,有些是通过自动化埋点,不需要开发手动上报,有些是需要开发自己上报。在我们业务的一个项目中,因为采用的是业务推动迭代的方式,有大量的跟业务相关的埋点,且变动比较大,所以我们基本上都是开发自己上报。

在之前的 Java 作为主力语言开发的时候,埋点参数上报的封装基本上是利用 Builder 模式,或者使用 Map 的方式来进行上报,而到了 Kotlin 作为主力开发语言后,可以利用 Kotlin 提供的一些语法,更好的封装实现埋点的上报,今天就来谈谈我在实际项目中封装的一种思路。

旧方式

原来的埋点上报,对于上报参数的组织,有些是通过 Builder,有些是利用 Map 来组织。我这里取神策埋点 SDK 的上报方式为例:

利用 JSONObject 构建 Json 数据的方式上报

java 复制代码
try {
    JSONObject newProperties = new JSONObject();
    newProperties.put("AdSource", "Email");
    properties.put("AdSource", "XXX Store");

    // 再次设定用户渠道,设定无效,"developer@sensorsdata.cn" 的 "AdSource" 属性值仍然是 "XXX Store"
    SensorsDataAPI.sharedInstance().profileSetOnce(newProperties);
} catch (JSONException e) {
    e.printStackTrace();
}

利用 Map 集合的方式上报:

java 复制代码
// map集合
Map<String, Number> properties = new HashMap<String, Number>();
properties.put("UserPaid", 1);
properties.put("PointEarned", 12.5);

SensorsDataAPI.sharedInstance().profileIncrement(properties);

个人在使用的时候,觉得这种上报参数的组织方式,比较零散,不易于管理,复用性不强。我们之前也是使用了差不多的上报方式,但是使用多了之后,发现不是很方便,因此基于 Kotlin 的委托的语法特性,重新进行了封装。

新思路

基础抽象类 BaseEventTrack

首先对于每个埋点,我们采用了类来进行管理,即每个埋点抽象成一个埋点类。下每个埋点类基础一个抽象类,这个抽象类,来定义一些基础的信息,大概如下:

kotlin 复制代码
abstract class BaseEventTrack {
    private val tag = "BaseEventTrack"
    // 所有上报参数得收集,这一块可以自己定义和修改
    private val paramsMap = hashMapOf<String, Any?>()
    // 埋点名字
    abstract val eventId: String
    // 埋点的归类
    abstract val eventTrackCategory: String

    /**
     * 触发上报当前的埋点
     */
    fun upload() {
        // 
        EventTrack.stat(eventTrackCategory, eventId, paramsMap)
    }
    /**
     * 定义每种类型的委托方法,控制它的get和set方法,如果有新增,可以在这里新增
     */
    protected fun int(): ReadWriteProperty<BaseEventTrack, Int?> = InnerReadWriteProperty<BaseEventTrack, Int?>(0)

    protected fun string(): ReadWriteProperty<BaseEventTrack, String?> =
        InnerReadWriteProperty<BaseEventTrack, String?>("")

    protected fun long(): ReadWriteProperty<BaseEventTrack, Long?> = InnerReadWriteProperty<BaseEventTrack, Long?>(0)
    protected fun boolean(): ReadWriteProperty<BaseEventTrack, Boolean?> =
        InnerReadWriteProperty<BaseEventTrack, Boolean?>(false)

    protected fun float(): ReadWriteProperty<BaseEventTrack, Float?> =
        InnerReadWriteProperty<BaseEventTrack, Float?>(0F)

    protected fun double(): ReadWriteProperty<BaseEventTrack, Double?> =
        InnerReadWriteProperty<BaseEventTrack, Double?>(0.0)

    // 代理类属性的处理
    inner class InnerReadWriteProperty<T, V>(defaultValue: V) : ReadWriteProperty<T, V> {
        private var currentValue: V = defaultValue
        override fun getValue(thisRef: T, property: KProperty<*>): V {
            Log.i(tag, "thisRef=${thisRef},property=${property}")
            return currentValue
        }

        override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
            Log.i(tag, "thisRef=${thisRef},property=${property},value=${value}")
            currentValue = value
            paramsMap[property.name] = value
        }
    }

}

这个埋点抽象类主要的作用在:

  • 定义了埋点一些基础属性,如 eventId 等
  • 利用 Kotlin 的属性委托,定义每种常用的基础数据类型的委托代理方法
  • 定义上报埋点的方法

这里新的思路就是:利用 Kotlin 中的属性委托类 ReadWriteProperty 来处理每个属性的 get 和 set,通过这个委托代理,收集和处理每个字段的值。其实就是起到一个拦截属性赋值和获取值的作用。

当有新的埋点时,我们可以这样定义埋点数据。比如我们常见的 Show 埋点:

kotlin 复制代码
class ShowEvent : BaseEventTrack() {
    override val eventId: String
        get() = "show"
    override val eventTrackCategory: String
        get() = "10001"

    /**
     * 页面停留时间
     */
    var pageTime by long()

    /**
     * 场景
     */
    var sceneName by string()

}

触发上报

埋点数据类封装好了之后,那么怎么给数据赋值和触发上报呢?这里使用inline函数,定义一个全局的静态方法,具体内容如下:

kotlin 复制代码
/**
 * @param block 初始化函数
 */
inline fun <reified T : BaseEventTrack> uploadEvent(crossinline block: T.() -> Unit) {
    kotlin.runCatching {
        // 实例化泛型T
        val clz = T::class.java
        val instance = clz.getDeclaredConstructor()
        instance.isAccessible = true
        val tInstance = instance.newInstance()
        // 触发对象初始化
        block.invoke(tInstance)
        // 触发上报
        tInstance.upload()
    }.onFailure {
        // 抛出异常,或者使用兜底方案
        Log.w("EventTrack", "upload event error,message=${it.message}")
    }
}

这个方法的参数是一个类型 T 的扩展函数类型,这个函数内部主要做的是初始化埋点数据字段的值。而方法内部的逻辑主要有:

  • 对泛型 T 进行实例化,构造对象
  • 触发对象的初始化操做,即对埋点字段赋值
  • 触发埋点的上报

注意:这里的方法中的reified必须加上,否则我们无法实例话一个泛型类型,这也是和 Java 不同的地方

比如上面的 Show 埋点,我们在使用的时候,可以这么使用:

kotlin 复制代码
fun uploadShowStat(){
    uploadEvent<ShowEvent> {
        sceneName = "七郎"
        pageTime = 100
    }
}

这样用起来是不是很方便呢~~~~

总结

这里的新思路,利用了 kotlin 的属性委托和reified的特性,实现了一种埋点参数封装的方式,这种方式优点是:使用管理起来非常的方便,每个埋点都对应一个类,代码可读性较好 。缺点是:过程中可能会参数一些额外的类。

当然上面只是提供了一种思路,一些细节没有提到,比如一些异常处理,兜底逻辑,线程安全等,有兴趣的可以在上面的基础上实现。

相关推荐
Joy T1 小时前
Agent 确定性上下文设计:从 Prompt 参数注入到能力分层
java·开发语言·微服务·架构·prompt·agent context
微订校园外卖平台4 小时前
同城外卖小程序开发:校园外卖系统四端架构与订单链路拆解
架构·外卖系统·本地生活·微订
m0_587383004 小时前
广州 24 小时自助健身房解决方案技术实现与实战指南
java·spring·小程序·架构·需求分析
牛战士coding4 小时前
AI Agent 如何接入真实业务系统?订单助手三层架构完整拆解
人工智能·架构
收放扳机5 小时前
PCB收放板设备机械手架构选型参考——三轴/四轴/六轴/蜘蛛手对比
人工智能·科技·架构·自动化·制造·pcb工艺
九卷5 小时前
企业级Agent系统整体技术架构介绍
ai·架构·大模型·llm·agent·智能体
木子n16 小时前
软件架构设计实战-架构18.从Classic到Adaptive:SOA架构在MATLAB里的模样
架构·autosar·性能工程·服务化架构
孟健7 小时前
从5000亿Token到83%字节匹配:AI逆向工程与重构的工程闭环实践
后端·架构·ai编程
geovindu8 小时前
sql:Search-Optimized SQL Queries using mysql 9.0
android·数据库·sql·mysql·数据库开发·数据库架构
CCblandy9 小时前
HTTP 长连接 3 类故障实测:[WinError 10053]、99 个 TIME_WAIT、69 次多余握手,4 种对策怎么选
后端·python·网络协议·http·云原生·性能优化·架构