Kotlin interface 详解:别再把接口写成空壳和万能开关

简介

接口最容易被写成两种极端。

一种是空壳:

kotlin 复制代码
interface UserService {
}

名字看起来很正规,里面没有任何契约。

另一种是万能开关:

kotlin 复制代码
interface UserService {
    fun login()
    fun logout()
    fun register()
    fun resetPassword()
    fun updateProfile()
    fun uploadAvatar()
    fun bindPhone()
    fun sendCoupon()
}

所有能力都塞进一个接口,实现类被迫实现一堆并不关心的方法。

Kotlin interface 真正适合做的事,是定义一组清楚的行为契约:

text 复制代码
能做什么
需要哪些属性
哪些行为有默认规则
不同实现之间如何替换

接口不是为了让代码看起来"更抽象",而是为了把变化点隔离出来。支付方式会变、日志落地方式会变、缓存实现会变、消息发送渠道会变,这些地方就很适合用接口。

Kotlin 的接口比 Java 早期接口更灵活。它可以有抽象函数、默认实现、抽象属性、计算属性、接口继承、多接口实现,还能配合 fun interfaceby 委托、sealed interface 做更干净的设计。

第一个接口:先定义能做什么

先看一个最小例子:通知发送。

kotlin 复制代码
interface Notifier {
    fun send(to: String, message: String)
}

这个接口只规定一件事:能发送消息。

邮件实现:

kotlin 复制代码
class EmailNotifier : Notifier {
    override fun send(to: String, message: String) {
        println("发送邮件到 $to:$message")
    }
}

短信实现:

kotlin 复制代码
class SmsNotifier : Notifier {
    override fun send(to: String, message: String) {
        println("发送短信到 $to:$message")
    }
}

调用方只依赖接口:

kotlin 复制代码
class RegisterService(
    private val notifier: Notifier
) {
    fun register(email: String) {
        println("创建账号:$email")
        notifier.send(email, "注册成功")
    }
}

fun main() {
    val service = RegisterService(EmailNotifier())
    service.register("tom@example.com")
}

输出:

text 复制代码
创建账号:tom@example.com
发送邮件到 tom@example.com:注册成功

后面要换成短信、站内信、测试假的通知器,RegisterService 不需要改。

kotlin 复制代码
val service = RegisterService(SmsNotifier())

这就是接口的核心价值:调用方依赖稳定契约,实现细节可以替换。

接口里的函数:没有函数体就是抽象函数

接口中的函数没有函数体时,实现类必须 override

kotlin 复制代码
interface Payment {
    fun pay(amount: Int): Boolean
}

class BalancePayment : Payment {
    override fun pay(amount: Int): Boolean {
        println("余额支付:$amount")
        return true
    }
}

class BankCardPayment : Payment {
    override fun pay(amount: Int): Boolean {
        println("银行卡支付:$amount")
        return true
    }
}

fun checkout(payment: Payment, amount: Int) {
    val success = payment.pay(amount)
    println("支付结果:$success")
}

fun main() {
    checkout(BalancePayment(), 100)
    checkout(BankCardPayment(), 200)
}

输出:

text 复制代码
余额支付:100
支付结果:true
银行卡支付:200
支付结果:true

checkout 不关心具体支付方式,只关心 Payment 契约。

接口变量可以接收任意实现类:

kotlin 复制代码
val payment: Payment = BalancePayment()

这就是多态。真正运行哪个 pay,由实际对象决定。

默认实现:公共流程可以放进接口

Kotlin 接口函数可以有函数体。

kotlin 复制代码
interface Logger {
    fun log(level: String, message: String)

    fun info(message: String) {
        log("INFO", message)
    }

    fun error(message: String) {
        log("ERROR", message)
    }
}

实现类只需要完成最核心的 log

kotlin 复制代码
class ConsoleLogger : Logger {
    override fun log(level: String, message: String) {
        println("[$level] $message")
    }
}

fun main() {
    val logger = ConsoleLogger()

    logger.info("服务启动")
    logger.error("连接失败")
}

输出:

text 复制代码
[INFO] 服务启动
[ERROR] 连接失败

infoerror 是接口里的默认实现。实现类可以直接复用,也可以覆盖。

kotlin 复制代码
class JsonLogger : Logger {
    override fun log(level: String, message: String) {
        println("""{"level":"$level","message":"$message"}""")
    }

    override fun error(message: String) {
        log("ERROR", "需要报警:$message")
    }
}

默认实现适合放"所有实现都大概率一致"的逻辑。不要把某个实现专属逻辑塞进接口默认方法。

接口属性:有名字,但没有字段

Kotlin 接口可以声明属性。

kotlin 复制代码
interface Account {
    val id: Long
    val name: String
}

data class UserAccount(
    override val id: Long,
    override val name: String
) : Account

接口属性也可以有默认 getter:

kotlin 复制代码
interface Account {
    val id: Long
    val name: String

    val displayName: String
        get() = "$id-$name"
}

data class UserAccount(
    override val id: Long,
    override val name: String
) : Account

fun main() {
    val account = UserAccount(1001, "Tom")
    println(account.displayName)
}

输出:

text 复制代码
1001-Tom

但接口属性不能这样写:

kotlin 复制代码
interface Account {
    val type = "user"
}

接口不能保存实例字段,也没有 backing field。属性只能是抽象属性,或者通过 getter 计算。

合法写法:

kotlin 复制代码
interface Account {
    val name: String

    val normalizedName: String
        get() = name.trim().lowercase()
}

需要保存状态时,状态必须放在实现类里:

kotlin 复制代码
class DefaultAccount(
    override val name: String
) : Account

var 属性也可以声明,但仍然不保存字段

接口可以声明 var

kotlin 复制代码
interface Switch {
    var enabled: Boolean

    fun turnOn() {
        enabled = true
    }

    fun turnOff() {
        enabled = false
    }
}

class LightSwitch : Switch {
    override var enabled: Boolean = false
}

fun main() {
    val switch = LightSwitch()

    println(switch.enabled)
    switch.turnOn()
    println(switch.enabled)
}

输出:

text 复制代码
false
true

enabled 的字段在 LightSwitch 里,不在接口里。

接口默认方法可以读写 enabled,但前提是实现类提供了这个属性。

接口继承接口:先拆能力,再组合能力

接口可以继承其他接口。

kotlin 复制代码
interface Readable {
    fun read(id: Long): String?
}

interface Writable {
    fun write(id: Long, value: String)
}

interface Repository : Readable, Writable

实现类需要补齐所有抽象成员:

kotlin 复制代码
class MemoryRepository : Repository {
    private val data = mutableMapOf<Long, String>()

    override fun read(id: Long): String? {
        return data[id]
    }

    override fun write(id: Long, value: String) {
        data[id] = value
    }
}

fun main() {
    val repository: Repository = MemoryRepository()

    repository.write(1, "Kotlin")
    println(repository.read(1))
}

输出:

text 复制代码
Kotlin

这种设计比一个大接口更灵活。

只读场景依赖 Readable

kotlin 复制代码
class QueryService(
    private val readable: Readable
) {
    fun query(id: Long): String {
        return readable.read(id) ?: "空"
    }
}

写入场景依赖 Writable

kotlin 复制代码
class SaveService(
    private val writable: Writable
) {
    fun save(id: Long, value: String) {
        writable.write(id, value)
    }
}

接口拆小之后,调用方只拿到真正需要的能力。

一个类实现多个接口

Kotlin 类只能继承一个父类,但可以实现多个接口。

kotlin 复制代码
interface Flyable {
    fun fly()
}

interface Swimmable {
    fun swim()
}

class Duck : Flyable, Swimmable {
    override fun fly() {
        println("低空飞行")
    }

    override fun swim() {
        println("水中游动")
    }
}

fun main() {
    val duck = Duck()

    duck.fly()
    duck.swim()
}

输出:

text 复制代码
低空飞行
水中游动

接口更像"能力标签 + 行为契约"。一个对象可以同时具备多个能力。

默认实现冲突:必须手动说清楚

多个接口里有相同签名的默认方法时,实现类必须覆盖。

kotlin 复制代码
interface Clickable {
    fun show() {
        println("Clickable show")
    }
}

interface Focusable {
    fun show() {
        println("Focusable show")
    }
}

class Button : Clickable, Focusable {
    override fun show() {
        super<Clickable>.show()
        super<Focusable>.show()
        println("Button show")
    }
}

fun main() {
    Button().show()
}

输出:

text 复制代码
Clickable show
Focusable show
Button show

super<Clickable>.show() 表示调用 Clickable 的默认实现。

super<Focusable>.show() 表示调用 Focusable 的默认实现。

如果不覆盖,编译器不知道该选哪一个,所以会直接报错。

属性默认 getter 也会冲突:

kotlin 复制代码
interface A {
    val title: String
        get() = "A"
}

interface B {
    val title: String
        get() = "B"
}

class C : A, B {
    override val title: String
        get() = super<A>.title + super<B>.title
}

fun main() {
    println(C().title)
}

输出:

text 复制代码
AB

冲突处理的原则很简单:多个来源都能提供实现时,最终类必须明确选择。

接口中的访问修饰符

接口成员默认是 public

常规接口成员不适合写 protected。接口表达的是公开契约,受保护状态和继承层级细节更适合放到抽象类。

接口内部可以放私有辅助方法,用来服务默认实现:

kotlin 复制代码
interface TokenParser {
    fun parse(raw: String): Map<String, String> {
        return raw
            .split(";")
            .mapNotNull { parsePair(it) }
            .toMap()
    }

    private fun parsePair(text: String): Pair<String, String>? {
        val parts = text.split("=")
        if (parts.size != 2) {
            return null
        }

        return parts[0].trim() to parts[1].trim()
    }
}

class DefaultTokenParser : TokenParser

fun main() {
    val parser = DefaultTokenParser()
    println(parser.parse("id=100; name=Tom; broken"))
}

输出:

text 复制代码
{id=100, name=Tom}

私有方法不能成为接口契约,只是接口内部默认实现的工具。

匿名对象:临时实现一个接口

有些接口实现只在一个地方使用,单独建类反而啰嗦。

kotlin 复制代码
interface Task {
    fun run()
}

fun execute(task: Task) {
    println("任务开始")
    task.run()
    println("任务结束")
}

fun main() {
    execute(object : Task {
        override fun run() {
            println("清理临时文件")
        }
    })
}

输出:

text 复制代码
任务开始
清理临时文件
任务结束

object : Task { ... } 创建了一个匿名对象。

匿名对象适合一次性实现。复用次数多、逻辑复杂、需要单元测试时,应该抽成具名类。

fun interface:一个抽象方法时可以用 Lambda

只有一个抽象方法的接口,可以声明为函数式接口。

kotlin 复制代码
fun interface Validator<T> {
    fun validate(value: T): Boolean
}

使用 Lambda 创建实例:

kotlin 复制代码
fun main() {
    val notBlank = Validator<String> { value ->
        value.isNotBlank()
    }

    println(notBlank.validate("Kotlin"))
    println(notBlank.validate("   "))
}

输出:

text 复制代码
true
false

函数式接口里可以有默认方法,但只能有一个抽象方法。

kotlin 复制代码
fun interface RetryPolicy {
    fun shouldRetry(error: Throwable): Boolean

    fun describe(): String {
        return "重试策略"
    }
}

使用:

kotlin 复制代码
fun request(policy: RetryPolicy) {
    val error = IllegalStateException("timeout")

    if (policy.shouldRetry(error)) {
        println("准备重试")
    } else {
        println("不再重试")
    }
}

fun main() {
    request { error ->
        error.message == "timeout"
    }
}

输出:

text 复制代码
准备重试

如果只是接收一个函数,普通函数类型通常更轻:

kotlin 复制代码
fun filterUsers(
    names: List<String>,
    predicate: (String) -> Boolean
): List<String> {
    return names.filter(predicate)
}

如果这个回调有明确业务含义,或者需要额外默认方法,fun interface 更合适:

kotlin 复制代码
fun interface UserNameRule {
    fun allow(name: String): Boolean

    fun errorMessage(): String {
        return "用户名不符合规则"
    }
}

SAM 和普通函数类型的差别

下面两个类型看起来很像:

kotlin 复制代码
fun interface IntRule {
    fun accept(value: Int): Boolean
}

typealias IntPredicate = (Int) -> Boolean

使用时都能接收 Lambda:

kotlin 复制代码
val rule = IntRule { it > 0 }
val predicate: IntPredicate = { it > 0 }

但它们不是同一种东西。

IntRule 是一个接口类型,可以拥有名字、默认方法、继承其他接口,也可以被 Java 调用方当成接口实现。

IntPredicate 只是函数类型别名,本质仍然是 (Int) -> Boolean

API 只是需要执行一个函数时,函数类型足够。

API 想表达领域契约时,函数式接口更清楚。

接口委托 by:少写一堆转发方法

接口常和委托一起使用。

先定义一个缓存接口:

kotlin 复制代码
interface Cache {
    fun get(key: String): String?
    fun put(key: String, value: String)
    fun remove(key: String)
}

内存实现:

kotlin 复制代码
class MemoryCache : Cache {
    private val data = mutableMapOf<String, String>()

    override fun get(key: String): String? {
        return data[key]
    }

    override fun put(key: String, value: String) {
        data[key] = value
    }

    override fun remove(key: String) {
        data.remove(key)
    }
}

如果要加日志,传统代理写法要手动转发所有方法:

kotlin 复制代码
class LoggingCache(
    private val cache: Cache
) : Cache {
    override fun get(key: String): String? {
        println("读取缓存:$key")
        return cache.get(key)
    }

    override fun put(key: String, value: String) {
        println("写入缓存:$key")
        cache.put(key, value)
    }

    override fun remove(key: String) {
        println("删除缓存:$key")
        cache.remove(key)
    }
}

使用 by 可以少写很多样板:

kotlin 复制代码
class LoggingCache(
    private val cache: Cache
) : Cache by cache {
    override fun get(key: String): String? {
        println("读取缓存:$key")
        return cache.get(key)
    }

    override fun put(key: String, value: String) {
        println("写入缓存:$key")
        cache.put(key, value)
    }
}

没有覆盖的 remove 会自动委托给 cache

完整使用:

kotlin 复制代码
fun main() {
    val cache: Cache = LoggingCache(MemoryCache())

    cache.put("token", "abc")
    println(cache.get("token"))
    cache.remove("token")
    println(cache.get("token"))
}

输出:

text 复制代码
写入缓存:token
读取缓存:token
abc
读取缓存:token
null

remove 没有日志,因为它直接走了被委托对象。

委托适合代理、装饰器、组合复用。接口越稳定,委托越省事。

多接口委托:组合多个小能力

一个类可以把不同接口委托给不同对象。

kotlin 复制代码
interface Printer {
    fun print(text: String)
}

interface Scanner {
    fun scan(): String
}

class ConsolePrinter : Printer {
    override fun print(text: String) {
        println("打印:$text")
    }
}

class FileScanner : Scanner {
    override fun scan(): String {
        return "扫描内容"
    }
}

class OfficeMachine(
    printer: Printer,
    scanner: Scanner
) : Printer by printer, Scanner by scanner

fun main() {
    val machine = OfficeMachine(ConsolePrinter(), FileScanner())

    machine.print("合同")
    println(machine.scan())
}

输出:

text 复制代码
打印:合同
扫描内容

这种方式比继承层级更轻。对象能力来自组合,而不是来自又深又长的父类链。

实战 Demo:支付策略 + 风控 + 日志

下面用接口拆一个支付流程。

金额类型:

kotlin 复制代码
data class Money(
    val cents: Long,
    val currency: String = "CNY"
) {
    fun text(): String {
        return "$currency ${cents / 100}.${(cents % 100).toString().padStart(2, '0')}"
    }
}

支付接口:

kotlin 复制代码
interface PaymentMethod {
    val name: String

    fun pay(orderId: Long, amount: Money): PaymentResult

    fun support(amount: Money): Boolean {
        return amount.cents > 0
    }
}

结果模型:

kotlin 复制代码
data class PaymentResult(
    val success: Boolean,
    val message: String
)

两个实现:

kotlin 复制代码
class BalancePay(
    private var balance: Long
) : PaymentMethod {
    override val name: String = "余额支付"

    override fun pay(orderId: Long, amount: Money): PaymentResult {
        if (balance < amount.cents) {
            return PaymentResult(false, "余额不足")
        }

        balance -= amount.cents
        return PaymentResult(true, "订单 $orderId 使用 $name 成功")
    }
}

class CardPay : PaymentMethod {
    override val name: String = "银行卡支付"

    override fun pay(orderId: Long, amount: Money): PaymentResult {
        return PaymentResult(true, "订单 $orderId 使用 $name 成功")
    }
}

风控接口:

kotlin 复制代码
interface RiskControl {
    fun check(orderId: Long, amount: Money): Boolean
}

class SimpleRiskControl : RiskControl {
    override fun check(orderId: Long, amount: Money): Boolean {
        return amount.cents <= 100_000
    }
}

支付服务只依赖接口:

kotlin 复制代码
class PaymentService(
    private val riskControl: RiskControl
) {
    fun pay(
        orderId: Long,
        amount: Money,
        method: PaymentMethod
    ): PaymentResult {
        if (!method.support(amount)) {
            return PaymentResult(false, "支付方式不支持该金额")
        }

        if (!riskControl.check(orderId, amount)) {
            return PaymentResult(false, "风控拦截")
        }

        return method.pay(orderId, amount)
    }
}

运行:

kotlin 复制代码
fun main() {
    val service = PaymentService(SimpleRiskControl())

    val small = service.pay(
        orderId = 1001,
        amount = Money(9_900),
        method = BalancePay(balance = 20_000)
    )

    val large = service.pay(
        orderId = 1002,
        amount = Money(200_000),
        method = CardPay()
    )

    println(small)
    println(large)
}

输出:

text 复制代码
PaymentResult(success=true, message=订单 1001 使用 余额支付 成功)
PaymentResult(success=false, message=风控拦截)

这里的变化点很清楚:

text 复制代码
支付方式会变,所以抽成 PaymentMethod
风控规则会变,所以抽成 RiskControl
支付编排流程相对稳定,所以放在 PaymentService

接口不是越多越好。只有变化点明确时,接口才有价值。

实战 Demo:Repository 接口不要只会 CRUD

很多项目会直接写:

kotlin 复制代码
interface UserRepository {
    fun save(user: User)
    fun delete(id: Long)
    fun update(user: User)
    fun findById(id: Long): User?
    fun findAll(): List<User>
}

这不一定错,但容易变成数据库操作清单。

更贴近业务的接口可以这样写:

kotlin 复制代码
data class User(
    val id: Long,
    val name: String,
    val enabled: Boolean
)

interface UserReader {
    fun findActiveUser(id: Long): User?
}

interface UserWriter {
    fun create(user: User)
    fun disable(id: Long)
}

内存实现:

kotlin 复制代码
class MemoryUserRepository : UserReader, UserWriter {
    private val users = mutableMapOf<Long, User>()

    override fun findActiveUser(id: Long): User? {
        return users[id]?.takeIf { it.enabled }
    }

    override fun create(user: User) {
        users[user.id] = user
    }

    override fun disable(id: Long) {
        val user = users[id] ?: return
        users[id] = user.copy(enabled = false)
    }
}

查询服务只依赖读接口:

kotlin 复制代码
class UserQueryService(
    private val userReader: UserReader
) {
    fun profile(id: Long): String {
        val user = userReader.findActiveUser(id) ?: return "用户不存在"
        return "${user.id}:${user.name}"
    }
}

运行:

kotlin 复制代码
fun main() {
    val repository = MemoryUserRepository()
    repository.create(User(1, "Tom", enabled = true))

    val service = UserQueryService(repository)

    println(service.profile(1))
    repository.disable(1)
    println(service.profile(1))
}

输出:

text 复制代码
1:Tom
用户不存在

接口按读写能力拆开后,调用方不会拿到不该使用的写权限。

实战 Demo:用 sealed interface 表达有限状态

普通接口表示开放扩展,任何地方都可能新增实现。

有些场景只允许有限几种状态,例如页面加载状态。

kotlin 复制代码
sealed interface UiState<out T>

data object Loading : UiState<Nothing>

data class Success<T>(
    val data: T
) : UiState<T>

data class Failure(
    val message: String
) : UiState<Nothing>

处理状态:

kotlin 复制代码
fun render(state: UiState<List<String>>): String {
    return when (state) {
        Loading -> "加载中"
        is Success -> "数据:${state.data.joinToString()}"
        is Failure -> "错误:${state.message}"
    }
}

fun main() {
    println(render(Loading))
    println(render(Success(listOf("Kotlin", "Java"))))
    println(render(Failure("网络异常")))
}

输出:

text 复制代码
加载中
数据:Kotlin, Java
错误:网络异常

sealed interface 的直接实现受限制,编译器知道有哪些分支。when 可以做穷尽检查,不容易漏掉状态。

普通接口适合开放插件式扩展。

密封接口适合有限状态、有限事件、有限错误类型。

接口和抽象类怎么选

接口和抽象类都能写抽象函数,也都能提供默认实现。差别主要在状态和继承模型。

对比点 interface abstract class
构造函数 没有构造函数 可以有构造函数
实例字段 不能保存字段 可以保存字段
实现数量 一个类可实现多个接口 一个类只能继承一个父类
主要表达 能力、契约、角色 共同基类、共享状态、模板流程
适合场景 支付方式、通知渠道、缓存能力、回调规则 带公共字段的基类、固定生命周期、模板方法

接口适合:

text 复制代码
只关心行为契约
实现之间没有共同状态
一个类需要组合多个能力
调用方只需要依赖最小能力

抽象类适合:

text 复制代码
需要构造参数
需要共享字段
需要受保护方法
需要固定一套继承模板

例子:

kotlin 复制代码
abstract class BaseController(
    protected val logger: Logger
) {
    fun handle(name: String) {
        logger.info("开始处理:$name")
        doHandle(name)
        logger.info("处理结束:$name")
    }

    protected abstract fun doHandle(name: String)
}

这里需要构造参数和 protected 成员,抽象类更合适。

接口不是 DTO,也不是标记贴纸

空接口有时会被当成标记:

kotlin 复制代码
interface DomainEvent

如果只是为了给类型贴标签,要确认后面是否真的会基于这个标签做约束。

更清楚的写法可能是密封接口:

kotlin 复制代码
sealed interface DomainEvent

data class UserRegistered(
    val userId: Long
) : DomainEvent

data class OrderPaid(
    val orderId: Long
) : DomainEvent

事件处理:

kotlin 复制代码
fun handle(event: DomainEvent): String {
    return when (event) {
        is UserRegistered -> "用户注册:${event.userId}"
        is OrderPaid -> "订单支付:${event.orderId}"
    }
}

空接口不是完全不能用,但要有明确目的。只是为了"看起来有架构",通常没有意义。

接口不要一上来就抽

代码里只有一个实现,而且短期没有第二个实现时,接口不一定有价值。

下面这种结构很常见:

kotlin 复制代码
interface UserService {
    fun register(name: String)
}

class UserServiceImpl : UserService {
    override fun register(name: String) {
        println("注册:$name")
    }
}

如果项目里永远只通过 UserServiceImpl 工作,接口只是多了一层文件。

接口值得出现的信号通常是:

text 复制代码
存在多个真实实现
测试需要替换实现
模块边界需要隔离
第三方依赖需要包一层
领域变化点已经明确
框架要求依赖接口

没有这些信号时,直接写类往往更简单。

常见坑一:接口太大

大接口会强迫实现类实现不需要的能力。

kotlin 复制代码
interface Worker {
    fun code()
    fun design()
    fun test()
    fun deploy()
}

如果有的角色只负责测试,也必须实现 codedesigndeploy,这就是接口设计过大。

拆成小接口:

kotlin 复制代码
interface Coder {
    fun code()
}

interface Designer {
    fun design()
}

interface Tester {
    fun test()
}

interface Deployer {
    fun deploy()
}

组合使用:

kotlin 复制代码
class FullStackEngineer : Coder, Designer, Tester, Deployer {
    override fun code() = println("编码")
    override fun design() = println("设计")
    override fun test() = println("测试")
    override fun deploy() = println("部署")
}

接口越小,调用方依赖越准。

常见坑二:默认实现塞太多业务

默认实现应该表达通用逻辑。

下面这种接口不太合适:

kotlin 复制代码
interface OrderProcessor {
    fun process(orderId: Long) {
        // 查询订单
        // 扣库存
        // 发起支付
        // 写日志
        // 推送消息
    }
}

接口默认方法里藏一整套业务流程,实现类很难知道哪些步骤可覆盖,哪些步骤不能碰。

复杂编排更适合放到服务类或抽象类。接口保留关键变化点。

常见坑三:接口命名过虚

这些名字很容易变成垃圾桶:

kotlin 复制代码
interface Manager
interface Handler
interface Processor
interface Helper

不是完全不能用,但单独看不出契约。

更好的名字要表达能力:

kotlin 复制代码
interface TokenVerifier
interface OrderPriceCalculator
interface InvoiceSender
interface UserReader
interface CacheStore

接口名最好能让函数列表变得可预测。

常见坑四:为了测试强行给所有类加接口

测试替换实现是接口的常见用途,但不代表所有类都要配一个接口。

纯计算类通常可以直接测试:

kotlin 复制代码
class PriceCalculator {
    fun discount(price: Int, rate: Int): Int {
        return price * (100 - rate) / 100
    }
}

更需要接口的是外部依赖:

kotlin 复制代码
interface Clock {
    fun nowMillis(): Long
}

class SystemClock : Clock {
    override fun nowMillis(): Long {
        return System.currentTimeMillis()
    }
}

测试时替换时间:

kotlin 复制代码
class FixedClock(
    private val fixed: Long
) : Clock {
    override fun nowMillis(): Long {
        return fixed
    }
}

外部系统、时间、随机数、网络、数据库、消息队列,这些地方更值得抽接口。

Java 互操作和 JVM default 方法

Kotlin 接口编译到 JVM 后,默认方法会按编译器配置生成。

常见配置在 Gradle Kotlin DSL 里:

kotlin 复制代码
kotlin {
    compilerOptions {
        jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
    }
}

常见模式:

text 复制代码
enable:默认模式,生成接口默认方法,同时保留兼容桥接
no-compatibility:只生成接口默认方法,适合新项目
disable:不生成接口默认方法,只走兼容实现

新项目通常更关注生成结果简洁;旧项目或库项目更关注二进制兼容。

和 Java 混写时,还要注意 Kotlin internal 在字节码层面可能变成 public 加名称改写。公共库接口不要随便暴露内部语义。

一个完整 Demo:通知中心的接口设计

下面把普通接口、默认实现、函数式接口、接口委托串起来。

消息模型:

kotlin 复制代码
data class Notice(
    val receiver: String,
    val title: String,
    val content: String
)

data class SendResult(
    val success: Boolean,
    val detail: String
)

通道接口:

kotlin 复制代码
interface NoticeChannel {
    val channelName: String

    fun send(notice: Notice): SendResult

    fun format(notice: Notice): String {
        return "[$channelName] ${notice.title} - ${notice.content}"
    }
}

两个通道:

kotlin 复制代码
class EmailChannel : NoticeChannel {
    override val channelName: String = "Email"

    override fun send(notice: Notice): SendResult {
        return SendResult(
            success = true,
            detail = "邮件发送给 ${notice.receiver}:${format(notice)}"
        )
    }
}

class SmsChannel : NoticeChannel {
    override val channelName: String = "Sms"

    override fun send(notice: Notice): SendResult {
        return SendResult(
            success = true,
            detail = "短信发送给 ${notice.receiver}:${notice.content}"
        )
    }
}

发送前过滤规则:

kotlin 复制代码
fun interface NoticeRule {
    fun allow(notice: Notice): Boolean
}

日志装饰器:

kotlin 复制代码
class LoggingNoticeChannel(
    private val target: NoticeChannel
) : NoticeChannel by target {
    override fun send(notice: Notice): SendResult {
        println("开始发送:${target.channelName}")
        val result = target.send(notice)
        println("发送结束:${result.success}")
        return result
    }
}

通知中心:

kotlin 复制代码
class NoticeCenter(
    private val channel: NoticeChannel,
    private val rule: NoticeRule
) {
    fun publish(notice: Notice): SendResult {
        if (!rule.allow(notice)) {
            return SendResult(false, "规则拦截")
        }

        return channel.send(notice)
    }
}

运行:

kotlin 复制代码
fun main() {
    val center = NoticeCenter(
        channel = LoggingNoticeChannel(EmailChannel()),
        rule = NoticeRule { notice ->
            notice.receiver.isNotBlank() && notice.content.length <= 50
        }
    )

    val result = center.publish(
        Notice(
            receiver = "tom@example.com",
            title = "订单通知",
            content = "订单已经支付成功"
        )
    )

    println(result)
}

输出:

text 复制代码
开始发送:Email
发送结束:true
SendResult(success=true, detail=邮件发送给 tom@example.com:[Email] 订单通知 - 订单已经支付成功)

这个例子里:

text 复制代码
NoticeChannel 表示可发送通知的通道
EmailChannel 和 SmsChannel 是不同实现
NoticeRule 是单方法规则,适合 fun interface
LoggingNoticeChannel 使用 by 委托增强行为
NoticeCenter 只依赖接口,不绑定具体通道

接口把变化点切开之后,新增飞书、钉钉、站内信,只需要加新的 NoticeChannel 实现。

写 interface 的几条实用规则

接口写得舒服,通常符合这些规则:

  • 名字表达能力,不表达空泛身份
  • 方法数量少,调用方只依赖必要能力
  • 默认实现只放稳定通用逻辑
  • 属性只表达契约,不假装保存字段
  • 有多个真实实现、边界隔离、测试替换时再抽接口
  • 单抽象方法且有业务语义时使用 fun interface
  • 有限状态和事件优先考虑 sealed interface
  • 多个默认实现冲突时显式覆盖并说明选择
  • 需要共享状态、构造参数、受保护成员时使用抽象类
  • 代理增强优先考虑接口委托 by

总结

Kotlin interface 不是"空壳文件",也不是"万能抽象层"。

它适合表达清楚的能力契约:

text 复制代码
PaymentMethod 能支付
RiskControl 能风控
UserReader 能读取用户
NoticeChannel 能发送通知
Cache 能读写缓存

Kotlin 接口的几个核心点:

text 复制代码
没有函数体的成员必须由实现类 override
有函数体的成员是默认实现
接口可以声明属性,但不能保存字段
一个类可以实现多个接口
多接口默认实现冲突必须显式覆盖
fun interface 适合单方法业务回调
sealed interface 适合有限状态建模
by 委托适合代理和组合复用

接口最好的状态,是调用方只看见稳定契约,实现类负责变化细节。抽象刚好挡住变化,代码才会变清楚;抽象挡住了阅读,接口就成了噪音。

相关推荐
dalong106 小时前
Savitzky-Golay 的C# 实现
开发语言·kotlin·c#
Android-Flutter9 小时前
kotlin 协程 - 核心概念
android·kotlin
Android-Flutter19 小时前
为什么说ActivityThread是主线程?
android·kotlin
学习使我健康1 天前
一个基于 **Jetpack Compose + Material 3** 的 Android 入门示例项目,演示 Compose 声明式 UI 的核心用法
ui·kotlin·android jetpack
alexhilton2 天前
Android XR 开发入门完全指南
android·kotlin·android jetpack
我命由我123453 天前
匈牙利命名法
java·服务器·后端·学习·java-ee·kotlin·学习方法
3 天前
Android 自定义 View 实战:从零打造工业级全向摇杆控件(OmniJoystickView)
android·kotlin·自定义view·摇杆控件
雨白3 天前
深入理解 Kotlin 协程 (十一):以逸待劳,探秘 select 多路复用与并发安全策略
android·kotlin
刘名喜3 天前
第22篇-数据库迁移-Liquibase
kotlin·springboot