Я не думаю, что есть универсальный ответ на этот вопрос ... Так что я просто дам вам свои 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
-метод, например:
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!")
Наконец, если у вас есть вспомогательные методы, такие как _31 _ (_ 32_ или что-то еще), из контекста также может быть ясно, что вы именно делаете. Самым важным, вероятно, является установление соглашения, которое будет понятно всем, чтобы они знали, когда применять то, что, а когда ожидать чего-то конкретного.
person
Roland
schedule
07.09.2018
val
в суперкласс, вы, по крайней мере, были бы вынуждены установить соответствующиеoverride
в подклассах данных ... Однако ... это может быть на один шаг лучше, чем необходимость копировать все самостоятельно ;-) Следующая проблема вероятно возникнет, когда вы захотите сопоставить или скопировать между ними. ;-) - person Roland   schedule 07.09.2018data class A(val a: String, val b: String) { constructor(b: String) : this("default", b) }
. - person Demigod   schedule 07.09.2018