如何在后期初始化中保留非空属性

时间:2018-09-07 00:23:13

标签: spring spring-boot kotlin dto data-class

以下问题:在具有Kotlindata class的客户端/服务器环境中,客户端希望创建A类型的对象,并因此通过RESTful端点将数据发布到服务器。

实体A在Kotlin中被实现为data class A(val mandatoryProperty: String) ,如下所示:

mandatoryProperty

从业务角度讲,该属性(也应为主键)绝对不能为null。但是,客户端不知道它,因为它是由服务器上的Spring @Service Bean昂贵地生成的。

现在,在端点处,Spring尝试将客户端的有效负载反序列化为类型A的对象,但是,data classes在那个时间点是未知的,这将导致映射异常。

几种解决该问题的方法,没有哪一种让我感到惊讶。

  1. 不要在端点处期望类型为A的对象,而是获得一堆描述A的参数,这些参数将一直传递到实际创建该实体并存在requiredProperty为止。实际上,这很麻烦,因为除了单个属性之外,还有很多其他属性。

  2. 非常类似于1,但是创建一个DTO。但是,我的最爱之一是,由于无法扩展@Pattern,这意味着将类型A的属性复制到DTO中(强制性属性除外)并将其复制过来。此外,当A增长时,DTO也必须增长。

  3. 使propertyProperty可为空并与!!整个代码中的运算符。可能是最糟糕的解决方案,因为它挫败了可为空和不可为空变量的含义。

  4. 客户端将为requiredProperty设置一个虚拟值,该属性在生成属性后立即被替换。但是,A由端点验证,因此伪值必须遵守其final AlertDialog.Builder mBuilder = new AlertDialog.Builder(ThisActivity.this); View view = getLayoutInflater().inflate(R.layout.yourpopup, null); final TextView text= view.findViewById(R.id.editText1); Button button= view.findViewById(R.id.button1); mBuilder.setView(view); final AlertDialog dialog = mBuilder.create(); mChangPwBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //something } }); dialog.show(); 约束。因此,每个虚拟值都将是有效的主键,这使我感到不舒服。

我可能还有其他更可行的监督方式吗?

1 个答案:

答案 0 :(得分:1)

我认为对此没有通用的答案...所以我只给你我2美分的款式...

您的第一个变体所具有的好处是其他任何人都没有的,即,您将不会将给定的对象用于其设计目的的其他任何用途(即,仅用于端点或后端),但是这可能会导致繁琐的开发

第二个变体很好,但是可能导致其他一些开发错误,例如当您以为使用了实际的A时,却选择使用DTO进行操作。

变体3和4在这方面类似于2 ...即使它仅具有DTO的所有属性,也可以将其用作A

所以...如果您想走安全路线,即没有人可以将此对象用于其他用途,那么它的特定用途可能应该使用第一个变量。 4听起来很像黑客。 2&3可能还可以。 3,因为当您将其用作DTO时实际上没有mandatoryProperty ...

仍然,因为您有自己喜欢的(2),我也有一个,所以我将专注于2和3,从2开始,使用带有sealed class作为超类型的子类方法:

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时,它甚至可能很清楚。在那里,什么时候没有...所以也展示了这个例子。首先从一个相当烦人的变体开始(令人讨厌的是,当您尝试访问尚未设置的变量时,它会抛出异常),但是至少您要保留!!null-在这里检查:

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)

最后一个是我的最爱,因为它需要最少的代码并使用val(因此也不能意外覆盖,或者您要对副本进行操作)。如果您不喜欢使用mandatoryProperty?,例如,

,也可以为!!添加访问器。
fun getMandatoryProperty() = mandatoryProperty ?: throw Exception("You didn't set it!")

最后,如果有适当的辅助方法,例如hasIDisDTO或其他),从上下文中也可以清楚地知道您正在做什么。最重要的可能是建立每个人都可以理解的约定,以便他们知道何时应用什么或何时期望某些特定的东西。