You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin Exposed中实体类能否自行实例化自身数据属性?

问题

我正在学习Kotlin以开发一款文本类游戏,为确保特定对象间的关系完整性(例如:Fuel实例不能被多个Machine实例使用),决定尝试使用Kotlin Exposed ORM框架。但我在理解Entity对象概念上遇到困难,以下是抽象基类Component的示例代码:

package machine.component

// ...imports

abstract class Component(id: EntityID<UUID>, var inputResources: Array<Resource>): UUIDEntity(id) {
    companion object : UUIDEntityClass<Component>(Components)
    val componentName: String by Components.componentName;
    // A map of how many resources are required to build the Component. Maps name of Resource class to quantity required
    abstract val _bluePrint: Map<KClass<out Resource>, Int>
    val resources by Resource optionalReferrersOn Resources.component;

    init {
        val sortedResources = this.resources.groupBy { it::class }
        sortedResources.forEach {
            if (this._bluePrint.keys.contains(it.key)) {
                require(it.value.count() == this._bluePrint[it.key]) {
                    "Component ${this.componentName} needs ${this._bluePrint[it.key]} '${it.key::class.simpleName} resource types to build"
                }
            }
        }
        // TODO: find a way to return the leftover resources to the user
        var leftoverResources: Map<KClass<out Resource>, List<Resource>> = this._resources.filterKeys { !this._bluePrint.keys.contains(it) }
        // Empty input array so it can't be used elsewhere
        inputResources.dropWhile { inputResources.isNotEmpty() }
    }
}

Component中引用的Resource实体类代码如下:

package resource

// ...imports

abstract class Resource (id: EntityID<UUID>): UUIDEntity(id) {
    companion object : UUIDEntityClass<Resource>(Resources)
    abstract val rawName: String;
    protected var hitPoints: Double by Resources.hitPoints;
    override fun toString(): String = "A ${rawName} resource with ${hitPoints} hitpoints"

    // TODO: make this function so that it can only accept an int from fuel being spent, not a random number
    fun extract(hits: Double): Resource? {
        hitPoints -= hits

        if (hitPoints <= 0) {
            return this
        }

        return null
    }
}

暂不考虑Kotlin基础层面的明显不足,该Entity类的设计意图如下:

  1. 用户实例化该类的子类,并传入一组(子类)Resource实例
  2. Component构造函数检查Resource类型,若特定资源数量不符合blueprint属性要求则初始化失败(我记得require会在断言为false时终止构造)
  3. 将所有剩余资源返回给用户
    4.(待实现)将蓝图所需资源存储到resources属性中
    5.(待实现)保存Component实例

我在步骤4遇到困难:实体类自行给自身赋值是否超出其职责范围?如果是,是否需要实现Repository/服务包装类执行验证检查并创建返回实例?若存在ComponentRepository,如何确保Component实例只能由它创建?如果最初的对象关系设计存在问题,也请指出。


回答

实体类职责边界问题

Entity的核心职责是映射数据库表结构、封装自身状态与基础业务行为,而资源校验、实例创建的复杂规则属于业务逻辑范畴,放在构造函数里确实违反单一职责原则——会让Entity既管数据持久化,又管业务校验,后续维护、扩展成本都会上升。

推荐方案:引入Repository/工厂类

把组件创建的验证、资源分配逻辑抽离到ComponentRepository或ComponentFactory中,具体实现思路:

  1. 限制Entity构造访问权限:将Component的构造函数设为internal或protected,仅允许同模块的Repository/工厂类调用,外部代码无法直接实例化。
  2. 在Repository中封装完整创建流程:
    • 接收用户传入的Resource列表
    • 校验资源数量是否匹配蓝图要求
    • 筛选出蓝图所需资源并关联到Component
    • 返回创建成功的Component和剩余资源
    • 统一处理Component的持久化操作

示例伪代码:

class ComponentRepository {
    fun buildComponent<T: Component>(componentClass: KClass<T>, inputResources: List<Resource>): Pair<T, List<Resource>> {
        // 1. 获取对应组件的蓝图
        val blueprint = getBlueprint(componentClass)
        // 2. 分组校验资源数量
        val groupedResources = inputResources.groupBy { it::class }
        blueprint.forEach { (resourceType, requiredCount) ->
            val actualCount = groupedResources[resourceType]?.size ?: 0
            require(actualCount >= requiredCount) {
                "组件需要${requiredCount}个${resourceType.simpleName},仅提供了${actualCount}个"
            }
        }
        // 3. 分离所需资源与剩余资源
        val requiredResources = inputResources.filter { blueprint.containsKey(it::class) }
        val leftoverResources = inputResources.filterNot { blueprint.containsKey(it::class) }
        // 4. 创建Component实例(构造为internal,此处可调用)
        val component = componentClass.constructors.first().call(EntityID(UUID.randomUUID(), Components), emptyArray())
        // 5. 关联资源(Exposed中通过外键字段赋值)
        requiredResources.forEach { it.component = component }
        // 6. 持久化操作
        transaction {
            component.save()
            requiredResources.forEach { it.save() }
        }
        return Pair(component as T, leftoverResources)
    }

    private fun getBlueprint(componentClass: KClass<out Component>): Map<KClass<out Resource>, Int> {
        // 通过反射或预定义映射获取组件蓝图
        return componentClass.objectInstance?._bluePrint ?: error("组件类未实现蓝图定义")
    }
}

如何确保Component仅由Repository创建?

  • 把Component的构造函数设为internal:如果Repository与Component在同一模块,即可调用构造;外部模块无法直接实例化。
  • 若要更严格,可将构造设为private,在Companion Object中暴露一个internal的创建方法,仅允许Repository调用。

初始设计的潜在问题

  1. 资源关联逻辑错误:当前Component的resources是optionalReferrersOn,意味着Resource主动关联Component,但你在init中尝试从this.resources读取传入的资源——此时Component未保存,resources集合为空,根本无法获取传入的inputResources,这是核心逻辑漏洞。
  2. 资源唯一性约束缺失:要确保Fuel不被多个Machine使用,需在Resource表的component外键添加唯一约束,或在业务逻辑中校验资源是否已关联其他Component。
  3. 构造函数资源清空无效:inputResources.dropWhile { inputResources.isNotEmpty() }不会修改原数组(dropWhile返回新集合),应改为inputResources = emptyArray()。
  4. 命名不规范:_bluePrint的下划线前缀通常表示私有变量,但此处是abstract val,外部需要访问,建议改为blueprint。

内容的提问来源于stack exchange,提问作者Scott Anderson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 14:15:28