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类的设计意图如下:
- 用户实例化该类的子类,并传入一组(子类)Resource实例
- Component构造函数检查Resource类型,若特定资源数量不符合blueprint属性要求则初始化失败(我记得require会在断言为false时终止构造)
- 将所有剩余资源返回给用户
4.(待实现)将蓝图所需资源存储到resources属性中
5.(待实现)保存Component实例
我在步骤4遇到困难:实体类自行给自身赋值是否超出其职责范围?如果是,是否需要实现Repository/服务包装类执行验证检查并创建返回实例?若存在ComponentRepository,如何确保Component实例只能由它创建?如果最初的对象关系设计存在问题,也请指出。
回答
实体类职责边界问题
Entity的核心职责是映射数据库表结构、封装自身状态与基础业务行为,而资源校验、实例创建的复杂规则属于业务逻辑范畴,放在构造函数里确实违反单一职责原则——会让Entity既管数据持久化,又管业务校验,后续维护、扩展成本都会上升。
推荐方案:引入Repository/工厂类
把组件创建的验证、资源分配逻辑抽离到ComponentRepository或ComponentFactory中,具体实现思路:
- 限制Entity构造访问权限:将Component的构造函数设为
internal或protected,仅允许同模块的Repository/工厂类调用,外部代码无法直接实例化。 - 在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调用。
初始设计的潜在问题
- 资源关联逻辑错误:当前Component的
resources是optionalReferrersOn,意味着Resource主动关联Component,但你在init中尝试从this.resources读取传入的资源——此时Component未保存,resources集合为空,根本无法获取传入的inputResources,这是核心逻辑漏洞。 - 资源唯一性约束缺失:要确保Fuel不被多个Machine使用,需在Resource表的
component外键添加唯一约束,或在业务逻辑中校验资源是否已关联其他Component。 - 构造函数资源清空无效:
inputResources.dropWhile { inputResources.isNotEmpty() }不会修改原数组(dropWhile返回新集合),应改为inputResources = emptyArray()。 - 命名不规范:
_bluePrint的下划线前缀通常表示私有变量,但此处是abstract val,外部需要访问,建议改为blueprint。
内容的提问来源于stack exchange,提问作者Scott Anderson
相关产品推荐
相关产品推荐

