代码之家  ›  专栏  ›  技术社区  ›  Jan B. HDR

如何在后期初始化时保留不可为空的属性

  •  1
  • Jan B. HDR  · 技术社区  · 6 年前

    以下问题:在具有 Spring-Boot Kotlin 客户机希望创建类型为A的对象,因此通过RESTful端点将数据发布到服务器。

    data class

    data class A(val mandatoryProperty: String)
    

    从业务角度来看,属性(也是主键)决不能为null。但是,客户机并不知道它,因为它是由服务器上的Spring@servicebean生成的,成本相当高。

    mandatoryProperty 此时未知,这将导致映射异常。

    有几种方法可以避免这个问题,但没有一种真正让我吃惊。

    1. 不要期望在端点处有一个类型为A的对象,而是获取一组描述A的参数,这些参数在实体实际创建并且mandatoryProperty存在之前一直传递。实际上相当麻烦,因为这里的房产远不止这一个。

    2. 与1非常相似,但创建一个DTO。不过,我最喜欢的一个是 data classes 无法扩展这意味着要将类型A的属性复制到DTO中(强制属性除外)并将它们复制过来。此外,当A增长时,DTO也必须增长。

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

    4. 客户机将为mandatoryProperty设置一个伪值,该值在生成属性后立即被替换。但是,A由端点验证,因此伪值必须遵守其 @Pattern

    有没有其他更可行的方法?

    1 回复  |  直到 6 年前
        1
  •  1
  •   Roland    6 年前

    我不认为有一个通用的答案。。。所以我会给你我的2美分关于你的变种。。。

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

    第二个变体很好,但可能会导致其他一些开发错误,例如,当您认为您使用了实际的 A 但你在做DTO手术。

    即使它只有DTO的所有属性。

    所以。。。如果你想走一条安全的路线,也就是说,任何人都不应该将这个对象用于任何其他用途,那么它的特定用途你可能应该使用第一个变体。4听起来像黑客。2&3可能还可以。因为你实际上没有 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 维切弗萨呢?我将由你决定。这里有几种方法(手动、使用反射或映射实用程序等)。 这个变体将所有特定于DTO的属性和非特定于DTO的属性清晰地分开。但是,它也会导致冗余代码(所有 override

    像3这样的东西可能更容易设置和维护(关于 data class 如果你正确地设置了边界,当有一个 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...
    

    但是:尽管 将抛出一个 Exception 在屏幕上看不到 toString 如果您需要检查它是否已经初始化(即使用 ::mandatoryProperty::isInitialized ).

    所以我给你看另一个变种(同时我最喜欢的,但是。。。使用 无效的 ):

    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 强制性财产 如果你不喜欢用 ? !! ,例如。

    fun getMandatoryProperty() = mandatoryProperty ?: throw Exception("You didn't set it!")
    

    hasID ( isDTO 或者随便什么)从上下文中也可以清楚地知道你到底在做什么。最重要的可能是建立一个每个人都能理解的惯例,这样他们就知道什么时候应用什么或者什么时候期望某个特定的东西。