聊聊 Kotlin 代理的“缺陷”与应对

简介: 聊聊 Kotlin 代理的“缺陷”与应对

Kotlin 代理是面试中经常被问到的问题,比如介绍一下代理的实现原理以及在使用中的一些注意事项等,本文将带你梳理这些问题,让你从更高维度上认识“代理”

Kotlin 有很多让人津津乐道的语法,“代理”就是经常被提及的一个。Kotlin 在语言级别通过 by 关键字支持了代理模式的实现。代理模式是最常用的设计模式之一,它是使用“组合”替代“继承”的最佳实践。下面取自 Wiki 中关于代理模式的例子:

image.png

class Rectangle(val width: Int, val height: Int) {
    fun area() = width * height
}
class Window(val bounds: Rectangle) {
    // Delegation
    fun area() = bounds.area()
}

这是一个代理模式的典型场景:Windowarea() 的具体实现委托给了 Retangle 类型对象 boundsRectangleWindow代理接收的关系。如果我们使用 Kotlin 的 by 关键字实现同样逻辑,代码变成下面这样:

interface ClosedShape {
    fun area(): Int
}
class Rectangle(val width: Int, val height: Int) : ClosedShape {
    override fun area() = width * height
}
class Window(private val bounds: ClosedShape) : ClosedShape by bounds

Kotlin 的 by 关键字只能基于接口进行代理,所以我们需要抽象出 WindowRectangle 的共同接口 ClosedShape,通过 by 关键字, Windowarea() 委托给 bounds 来实现, Window 内部中省掉了直接调用 bounds 的代码。这个例子比较简单,优势体现的不明显,试想随着接口方法的增多,by 可以帮我们减少大量的模板代码。

虽然 by 关键字为我们带来了方便,但是它的一些机制也受到不少开发者诟病,甚至连 Kotlin 首席设计师 Andrey Breslav 都曾公开表示不喜欢这个功能。Kotlin 接口代理被诟病的问题主要有两个:

  • 代理中无法访问 this
  • 代理无法运行时替换

缺陷1:代理中无法访问 "this"

代理与继承的一个重要区别在于,继承关系中父类可以通过 this 访问运行时的真正实例;而代理关系中代理无法通过 this 直接访问接收方对象(例子中的 Window),但有时我们确实需要获取接收方的状态参与计算,在 Java 中的常见做法是接收方在创建代理时注入自身实例。而 Kotlin 的 by 关键字需要在接收方实例化之前创建好代理,因此无法为代理注入 this 对象。

上面的例子中,假设 widthheightWindow 维护的状态而非 Rectangle,我们在 Rectanglearea() 中依赖它们来进行计算,此时该如何解决呢?一个可行的做法是在 Windowinit 中注入向 Rectangle 注入所需的状态。这里需要注意两点,

  • 第一,直接注入 width 和 height 是不行的,假设 Window 的 size 会变化,所以 Rectangle 需要在计算 area 时始终获取最新的数值,
  • 第二,注入 Window 实例作为 “this”,通过 this 获取最新的 widht 和 height?这也是不妥的,Rectangle 依赖 Window 类型,会降低 Rectangle 的可复用性。

兼顾上述两点后,更合理的做法是为 Rectangle 定义一个可以获取 width/height 的函数类型,然后由 Wiindow 注入这个回调,代码如下:

interface ClosedShape {
    fun area(): Int
}
class Rectangle : ClosedShape {
    lateinit var size: () -> Pair<Int, Int>
    override fun area() = size().let { it.first * it.second }
}
class Window(private val bounds: Rectangle) : ClosedShape by bounds {
    private var width: Int = TODO()
    private var height: Int = TODO()
    init {
        bounds.size = { width to height }
    }
}

也许有人会提议为 area() 增加参数,动态传入 widthheight,但是这增加了 Window 的调用方的负担,违背面向对象中封装性的设计原则。

缺陷2:无法运行时替换代理

不少人希望代理模式中的代理能够根据需要动态替换,实现类似策略模式的效果。但这在目前 Kotlin 代理中是无法实现的。不少 Kotlin 的初学者曾经误认为通过 var 替换代理实例,比如下面代码中,我们将 Window 的参数 bounds 的声明从 val 改为 var

class Window(private var bounds: ClosedShape) : ClosedShape by bounds

但是经编译后的代码实际是下面这样,代理存储在 bounds 之外的另一个 final 成员 ``$$delegate_0` 中。

public final class Window implements ClosedShape {
   private ClosedShape bounds;
   // $FF: synthetic field
   private final ClosedShape $$delegate_0;
   public Window(@NotNull ClosedShape bounds) {
      Intrinsics.checkNotNullParameter(bounds, "bounds");
      super();
      this.$$delegate_0 = bounds;
      this.bounds = bounds;
   }
   public int area() {
      return this.$$delegate_0.area();
   }
}

即使我们在运行时为 bounds 赋值新的对象,代理中的实例也不会发生变化。 假设有这样的场景, Window 的形状在运行时会发生变化,相应地我们需要计算 area 的代理由 Rectangle 变为 Oval,此时该如何解决呢? 一个不难想到的思路是:增加代理的“代理”,实现代理实例的可替换

class Proxy(var target: ClosedShape) : ClosedShape {
    override fun area() = target.area()
}
class Rectangle : ClosedShape {
    lateinit var size: () -> Pair<Int, Int>
    override fun area() = size().let { it.first * it.second }
}
class Oval : ClosedShape {
    lateinit var size: () -> Pair<Int, Int>
    override fun area() = size().let { Pi * it.first / 2 * it.second / 2 }
}
class Window(private val bounds: Proxy) : ClosedShape by bounds {
    private var width: Int = TODO()
    private var height: Int = TODO()
    private val rectangle by lazy {
        Rectangle().apply { size = { width to height } }
    }
    private val oval by lazy {
        Oval().apply { size = { width to height } }
    }
    fun changeShape(mode: Shape) {
        when (mode) {
            Rectangle -> bounds.target = rectangle
            Oval -> bounds.target = oval
        }
    }
}

上面代码中,我们定义了一个 Proxy 作为 Window 的代理,而真正被调用到的对象是 Proxytarget,它可以在运行时根据需要做出变化。

但这也带来一个问题,如果接口中的方法很多,Proxy 中会出现大量的 target 的转发代码,增加我们的工作量。此时我们可以使用动态代理对其优化:

class Proxy(var target: ClosedShape?)  {
    fun create() : ClosedShape {
        return newProxyInstance(
            ClosedShape::class.java.getClassLoader(), arrayOf<Class<*>>(ClosedShape::class.java), object : InvocationHandler {
                override fun invoke(proxy: Any?, method: Method, args: Array<out Any>?) = method.invoke(target, args)
            }
        ) as ClosedShape
    }
}
class Window(private val bounds: Proxy) : ClosedShape by bounds.create() {
    //...省略
}

上面代码中,Proxycreate() 返回一个动态代理对象,帮节省了原本需要手动实现的转发代码。

对比其他解决方案

通过上面分析我们知道,使用 by 关键字创建的代理需要在接收方(例子中的 Window)实例化之前确定,并且在编译后存储在一个不可见的 final 成员上,这使得接收方缺少对代理的直接控制的能力,比如无法在 Window 内创建代理,也无法在运行时替换代理。而对比 Kotlin 之外的其他同类解决方案中,你会发现接收方的控制力明显要强得多:

  • Lombook (Kotlin 出现前常用的语法糖工具)提供了 @Delegate 注解,它可以帮助我们将接收方的成员声明为代理,无需再通过构造函数传入,接收方可以在自行创建代理的同时方便地做一些注入工作;
  • Guava(Google 提供的 JDK 增强库)也提供了实现代理模式的 ForwardingObject,它允许我们在接收方内部通过重写 protected abstract Object delegate(); 返回最新的代理对象,实现代理的可替换。

因此,我们可以简单下一个结论:Kotlin 代理之所以被人诟病,其根本原因在于相对于其他同类方案,接收方缺少对代理的直接控制的能力。目前有不少开发者提了相关 Issue,也许可以期待 Kotlin 在未来的版本中出现更合理的解决方案。在此之前,我们只能通过本文介绍一些 Workaround 进行应对。需要注意本文讲的代理仅仅指接口代理,相比之下,属性代理的设计合理得多,不存在上述这些问题。

目录
相关文章
|
1月前
|
API 开发者 Kotlin
Kotlin 中如何使用 Fuel 库进行代理切换?
Kotlin 中如何使用 Fuel 库进行代理切换?
|
Kotlin
Kotlin的属性代理
一、定义一个方法 val/var : by (代理者)表达式 代理者需要实现相应的setValue/getValue 方法 如果是val,只需要设置getValue方法 如果是var,则需要设置setValue/getValue 方法 二、看一个属性代理的例子吧 package net.
1009 0
|
22天前
|
移动开发 数据库 Android开发
构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第30天】 在移动开发领域,性能优化和流畅的用户体验是至关重要的因素。对于Android开发者来说,Kotlin协程作为一种异步编程解决方案,提供了强大且轻量级的机制来处理后台任务,而不会对主线程造成阻塞。本文将深入探讨Kotlin协程的概念、优势以及如何在Android应用中实现它们,从而改善应用响应性和用户满意度。通过实例代码和最佳实践的分享,我们将展示如何有效利用协程来处理网络请求、数据库操作和耗时计算,同时确保UI的流畅性。
|
20天前
|
安全 Java Android开发
使用Kotlin进行Android应用开发:高效、简洁与乐趣并存
【6月更文挑战第1天】Kotlin,JetBrains开发的静态类型语言,正日益成为Android开发首选。它与Java兼容,提供简洁、安全的语法,如空安全、扩展函数和Lambda表达式,提升开发效率和代码可读性。Kotlin在Android开发中的优势包括提高开发速度、降低学习曲线及强大的社区支持。实践中,数据类简化对象创建,扩展函数增强SDK,Lambda表达式简化回调处理,协程优化异步操作。掌握Kotlin对Android开发者极具价值。
|
22天前
|
移动开发 调度 Android开发
构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第30天】在移动开发领域,Android平台的流畅性与效率一直是开发者追求的核心。随着Kotlin语言的普及,其提供的协程特性为编写高效、轻量级的异步代码提供了强大工具。本文将深入探讨如何在Android项目中利用Kotlin协程来优化性能,提升用户体验。我们将从协程的基本概念出发,通过实例演示如何在实际开发中合理运用协程,并讨论协程对资源管理和错误处理的影响。
20 3
|
21天前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。
|
22天前
|
移动开发 安全 Android开发
构建高效Android应用:Kotlin协程的实践与优化策略
【5月更文挑战第30天】 在移动开发领域,性能优化始终是关键议题之一。特别是对于Android开发者来说,如何在保证应用流畅性的同时,提升代码的执行效率,已成为不断探索的主题。近年来,Kotlin语言凭借其简洁、安全和实用的特性,在Android开发中得到了广泛的应用。其中,Kotlin协程作为一种新的并发处理机制,为编写异步、非阻塞性的代码提供了强大工具。本文将深入探讨Kotlin协程在Android开发中的应用实践,以及如何通过协程优化应用性能,帮助开发者构建更高效的Android应用。
|
3天前
|
安全 Java 编译器
Android面试题之Java 泛型和Kotlin泛型
**Java泛型是JDK5引入的特性,用于编译时类型检查和安全。泛型擦除会在运行时移除类型参数,用Object或边界类型替换。这导致几个限制:不能直接创建泛型实例,不能使用instanceof,泛型数组与协变冲突,以及在静态上下文中的限制。通配符如<?>用于增强灵活性,<? extends T>只读,<? super T>只写。面试题涉及泛型原理和擦除机制。
13 3
Android面试题之Java 泛型和Kotlin泛型
|
12天前
|
安全 Java Android开发
Kotlin与Java:Android开发的双剑合璧
【6月更文挑战第9天】Kotlin和Java在Android开发中形成互补态势。Java凭借广泛社区支持和丰富的类库资源占据主导,但其语法繁琐和空指针问题限制了发展。Kotlin,设计来解决这些问题,以其简洁、安全、高效的特性逐渐兴起。Kotlin的互操作性允许与Java无缝集成,提升开发效率,减少错误。两者结合提高了代码质量和开发者的灵活性,促进了Android开发社区的繁荣。开发者应把握这种&quot;双剑合璧&quot;,适应技术发展。
30 10
|
5天前
|
Android开发 Kotlin
Android面试题 之 Kotlin DataBinding 图片加载和绑定RecyclerView
本文介绍了如何在Android中使用DataBinding和BindingAdapter。示例展示了如何创建`MyBindingAdapter`,包含一个`setImage`方法来设置ImageView的图片。布局文件使用`&lt;data&gt;`标签定义变量,并通过`app:image`调用BindingAdapter。在Activity中设置变量值传递给Adapter处理。此外,还展示了如何在RecyclerView的Adapter中使用DataBinding,如`MyAdapter`,在子布局`item.xml`中绑定User对象到视图。关注公众号AntDream阅读更多内容。
14 1