Как сохранить ненулевые свойства при поздней инициализации - PullRequest
0 голосов
/ 07 сентября 2018

Следующая проблема: В среде клиент / сервер с Spring-Boot и Kotlin клиент хочет создать объекты типа A и, следовательно, отправляет данные через конечную точку RESTful на сервер.

Сущность A реализована как data class в Kotlin следующим образом:

data class A(val mandatoryProperty: String)

С точки зрения бизнеса это свойство (которое также является первичным ключом) никогда не должно быть нулевым. Однако клиент не знает об этом, поскольку он генерируется довольно дорого с помощью Spring @Service Bean на сервере.

Теперь в конечной точке Spring пытается десериализовать полезную нагрузку клиента в объект типа A, однако mandatoryProperty в этот момент времени неизвестен, что приведет к исключению отображения.

Несколько способов обойти эту проблему, ни один из которых меня не удивляет.

  1. Не ожидайте объект типа A в конечной точке, но получите набор параметров, описывающих A, которые передаются до тех пор, пока сущность не будет фактически создана и не присутствует обязательный свойства. На самом деле довольно громоздко, так как свойств гораздо больше, чем просто одного.

  2. Совершенно похоже на 1, но создайте DTO. Одна из моих любимых, однако, поскольку data classes не может быть расширена, это будет означать дублирование свойств типа A в DTO (за исключением обязательного свойства) и их копирование. Кроме того, когда А растет, DTO тоже должен расти.

  3. Сделайте обязательными свойства, обнуляемые и работайте с !! оператор по всему коду. Вероятно, наихудшее решение, так как оно мешает восприятию обнуляемых и ненулевых переменных.

  4. Клиент установит фиктивное значение для обязательного свойства, которое будет заменено, как только свойство будет сгенерировано. Однако A проверяется конечной точкой, и поэтому фиктивное значение должно подчиняться ограничению @Pattern. Поэтому каждое фиктивное значение будет действительным первичным ключом, что вызывает у меня плохое предчувствие.

Какие-нибудь другие способы, которые я мог бы осмотреть, более осуществимы?

1 Ответ

0 голосов
/ 07 сентября 2018

Я не думаю, что есть универсальный ответ на это ... Так что я просто дам вам 2 цента за ваши варианты ...

У вашего первого варианта есть преимущество, которого на самом деле нет у других, т. Е. Вы не будете использовать данные объекты для чего-то другого, чем они были предназначены (т. Е. Только для конечной точки или для серверных целей), что, однако, вероятно, приведет к громоздкой разработке. .

Второй вариант хорош, но может привести к некоторым другим ошибкам разработки, например, когда вы думали, что использовали реальный A, но вместо этого вы скорее работали на DTO.

Варианты 3 и 4 в этом отношении аналогичны 2 ... Вы можете использовать его как A, хотя он имеет все свойства только DTO.

Итак ... если вы хотите пойти по безопасному пути, то есть никто никогда не должен использовать этот объект для чего-то еще, кроме его конкретной цели, вам, вероятно, следует использовать первый вариант. 4 звучит скорее как хак. 2 и 3, вероятно, в порядке. 3 потому что у вас нет mandatoryProperty, когда вы используете его как DTO ...

Тем не менее, поскольку у вас есть ваш любимый (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 -метод, например ::1010**

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!")

Наконец, если у вас есть несколько вспомогательных методов, таких как hasID (isDTO или что-то еще), из контекста также может быть ясно, что именно вы делаете. Вероятно, наиболее важным является создание соглашения, которое все понимают, чтобы они знали, когда применять то, что и когда ожидать чего-то конкретного.

...