我不认为有一个通用的答案......所以我只会给你我的 2 美分关于你的变种......
您的第一个变体具有其他人真正没有的好处,即您不会将给定的对象用于其他任何设计(即仅用于端点或后端目的),但这可能会导致繁琐的开发。
第二个变体很好,但可能会导致一些其他开发错误,例如,当您认为您使用实际A
但您宁愿在 DTO 上操作时。
变体 3 和 4 在这方面与 2 相似......A
即使它仅具有 DTO 的所有属性,您也可以使用它。
所以...如果您想走安全的路线,即没有人应该将此对象用于其他任何事情,那么您可能应该使用第一个变体来实现其特定目的。4 听起来很像一个黑客。2 & 3 应该没问题。mandatoryProperty
3 因为当你将它用作 DTO 时,你实际上没有......
尽管如此,正如你有你最喜欢的 (2) 而我也有一个,我将专注于 2 和 3,从 2 开始,使用带有sealed class
as 超类型的子类方法:
sealed class AbstractA {
// just some properties for demo purposes
lateinit var sharedResettable: String
abstract val sharedReadonly: String
}
data class A(
val mandatoryProperty: Long = 0,
override val sharedReadonly: String
// we deliberately do not override the sharedResettable here... also for demo purposes only
) : AbstractA()
data class ADTO(
// this has no mandatoryProperty
override val sharedReadonly: String
) : AbstractA()
一些演示代码,演示用法:
// just some random setup:
val a = A(123, "from backend").apply { sharedResettable = "i am from backend" }
val dto = ADTO("from dto").apply { sharedResettable = "i am dto" }
listOf(a, dto).forEach { anA ->
// somewhere receiving an A... we do not know what it is exactly... it's just an AbstractA
val param: AbstractA = anA
println("Starting with: $param sharedResettable=${param.sharedResettable}")
// set something on it... we do not mind yet, what it is exactly...
param.sharedResettable = UUID.randomUUID().toString()
// now we want to store it... but wait... did we have an A here? or a newly created DTO?
// lets check: (demo purpose again)
when (param) {
is ADTO -> store(param) // which now returns an A
is A -> update(param) // maybe updated also our A so a current A is returned
}.also { certainlyA ->
println("After saving/updating: $certainlyA sharedResettable=${certainlyA.sharedResettable /* this was deliberately not part of the data class toString() */}")
}
}
// assume the following signature for store & update:
fun <T> update(param : T) : T
fun store(a : AbstractA) : A
样本输出:
Starting with: A(mandatoryProperty=123, sharedReadonly=from backend) sharedResettable=i am from backend
After saving/updating: A(mandatoryProperty=123, sharedReadonly=from backend) sharedResettable=ef7a3dc0-a4ac-47f0-8a73-0ca0ef5069fa
Starting with: ADTO(sharedReadonly=from dto) sharedResettable=i am dto
After saving/updating: A(mandatoryProperty=127, sharedReadonly=from dto) sharedResettable=57b8b3a7-fe03-4b16-9ec7-742f292b5786
我还没有向你展示丑陋的部分,但你自己已经提到过......你如何将你的转换ADTO
为A
反之亦然?我会把它留给你。这里有几种方法(手动,使用反射或映射实用程序等)。此变体将所有特定于 DTO 的属性与非特定于 DTO 的属性完全分开。然而,它也会导致冗余代码(所有的override
,等等)。但至少您知道您操作的是哪种对象类型,并且可以相应地设置签名。
像 3 这样的东西可能更容易设置和维护(关于它data class
本身 ;-)),如果你正确地设置了边界,它甚至可能很清楚,什么时候有一个null
,什么时候没有......所以也展示了这个例子。首先从一个相当烦人的变体开始(令人讨厌的是,当您尝试访问尚未设置的变量时,它会引发异常),但至少您可以在此处省去!!
or null
-checks:
data class B(
val sharedOnly : String,
var sharedResettable : String
) {
// why nullable? Let it hurt ;-)
lateinit var mandatoryProperty: ID // ok... Long is not usable with lateinit... that's why there is this ID instead
}
data class ID(val id : Long)
演示:
val b = B("backend", "resettable")
// println(newB.mandatoryProperty) // uh oh... this hurts now... UninitializedPropertyAccessException on the way
val newB = store(b)
println(newB.mandatoryProperty) // that's now fine...
但是:即使访问mandatoryProperty
会抛出一个Exception
它在 中不可见,toString
如果您需要检查它是否已经被初始化(即通过使用::mandatoryProperty::isInitialized
),它看起来也不好看。
所以我向你展示了另一个变体(同时我最喜欢,但是......使用null
):
data class C(val mandatoryProperty: Long?,
val sharedOnly : String,
var sharedResettable : String) {
// this is our DTO constructor:
constructor(sharedOnly: String, sharedResettable: String) : this(null, sharedOnly, sharedResettable)
fun hasID() = mandatoryProperty != null // or isDTO, etc. what you like/need
}
// note: you could extract the val and the method also in its own interface... then you would use an override on the mandatoryProperty above instead
// here is what such an interface may look like:
interface HasID {
val mandatoryProperty: Long?
fun hasID() = mandatoryProperty != null // or isDTO, etc. what you like/need
}
用法:
val c = C("dto", "resettable") // C(mandatoryProperty=null, sharedOnly=dto, sharedResettable=resettable)
when {
c.hasID() -> update(c)
else -> store(c)
}.also {newC ->
// from now on you should know that you are actually dealing with an object that has everything in place...
println("$newC") // prints: C(mandatoryProperty=123, sharedOnly=dto, sharedResettable=resettable)
}
最后一个有好处,您可以copy
再次使用 - 方法,例如:
val myNewObj = c.copy(mandatoryProperty = 123) // well, you probably don't do that yourself...
// but the following might rather be a valid case:
val myNewDTO = c.copy(mandatoryProperty = null)
最后一个是我最喜欢的,因为它需要最少的代码并使用 aval
代替(因此也不会意外覆盖,或者您改为在副本上操作)。mandatoryProperty
如果您不喜欢使用?
or ,您也可以只添加一个访问器!!
,例如
fun getMandatoryProperty() = mandatoryProperty ?: throw Exception("You didn't set it!")
最后,如果您有一些辅助方法,例如hasID
(isDTO
或其他),那么从上下文中也可以清楚您到底在做什么。最重要的可能是建立一个每个人都理解的约定,这样他们就知道什么时候应用什么或者什么时候期待一些特定的东西。