What's a compact and simple way to implement variants of object going through different states?
The complication is that for every state a new properties added to the same object.
A task executed and follow through states Created / Waiting / InProgress possible way would be
to define following classes
class Task(
val type: String,
val make_request: () -> Void
)
class TaskWaitingExecution(
val task: Task,
val retry_count: Int
)
class TaskInProgress(
val task: TaskWaitingExecution,
val request_id: Int
) {
val timer = bon.timer()
}
And progress with steps
val task = Task("some_request', {})
val waiting = TaskWaitingExecution(task, 1)
val in_progress = TaskInProgress(waiting, 10)
But it's not ideal as accessing say type property would end up in
in_progress.task.task.type
Sadly there's no way to use implements by inhreitance implement PreviousTask by task
as it's not working for classes.
Is there a better way to express such logic?
Turn it inside out and put the status as a property of the Task:
class Task(
val type: String,
val make_request: () -> Void,
var status: TaskStatus = null,
)
To store the status-specific fields, a sealed class is perfect:
sealed class TaskStatus;
class WaitingExecution : TaskStatus (
val retry_count: Int
)
class InProgress : TaskStatus (
val request_id: Int
) {
val timer = bon.timer()
}
Usage:
val task = Task("some_request', {})
task.status = WaitingExecution(1)
task.status = InProgress(10)
You can do it with val status as well, but then you'd need to create a new Task object each time its status changes.
It's been a while since I last Kotlin'ed so there might be syntax errors, but I hope the idea is clear.
Related
I have Nodes similar to this:
#Node
data class Actor (
#Id val id: UUID,
val name: String,
#Relationship(type = "ACTED_IN")
val films: Set<Film> = emptySet())
#Node
data class Film(
#Id val id: UUID,
val name: String,
#Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
val actors: Set<Actor> = emptySet())
Further more i have projection classes:
data class ActorProjection (
val name: String,
val films: Set<FilmProjection>)
data class FilmProjection(
val name: String)
Having a repository function as such:
#Repository
interface ActorRepository : Neo4jRepository<Actor, UUID> {
fun findByName(name: String): ActorProjection?
}
Using the function produces an exception:
The node with id 0 has a logical cyclic mapping dependency. Its creation caused the creation of another node that has a reference to this.
So I guess the domain class is used instead of the projection class to create the queries. How can I deal with this? Ofc I could delete the inverse Relationship in the Film class. However, I would like to have all associations visible in both classes for documentation and to also achieve the inverse projection. How to achieve this?
Edit:
Changing the node classes as such:
#Node
data class Actor (
#Id val id: UUID = UUID.randomUUID(),
val name: String
){
#Relationship(type = "ACTED_IN")
var films: Set<Film> = emptySet()
private set
}
#Node
data class Film(
#Id val id: UUID = UUID.randomUUID(),
val name: String) {
#Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
var actors: Set<Actor> = emptySet()
private set
}
I get rid of the exception. However the resulting ActorProjection has a Set of Films not FilmProjections when looking at it with the debugger. I guess its due to the generic type erasure. Is there a annotation or anything to give the type hint to neo4j?
I'm trying to come up with a data model that allows me to do the following:
Define a type of Task and change its Status
This Status can be either InProgress or Completed
In the case of a completed Status, I want to be able to add data, that is specific to the Task that was completed.
Initially, I came up with this data model:
sealed class Task<R : TaskResult> {
abstract val status: TaskStatus<R>
data class A(
val data: String,
override val status: TaskStatus<NoResult>,
) : Task<NoResult>()
data class B(
val data: String,
override val status: TaskStatus<TaskBResult>,
) : Task<TaskBResult>()
}
sealed class TaskStatus<R : TaskResult> {
object InProgress : TaskStatus<NoResult>()
data class Completed<R : TaskResult>(val result: R) : TaskStatus<R>()
}
sealed class TaskResult {
object NoResult : TaskResult()
data class TaskBResult(val resultData: String) : TaskResult()
}
Here you have Task.A and Task.B, where:
A completed Task.A only accepts NoResult
A completed Task.B only accepts TaskBResult
However, when I run this:
fun main() {
val taskA = Task.A(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = NoResult
)
)
val taskB = Task.B(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = TaskBResult(
resultData = "resultData"
)
)
)
}
I get the following compile error for setting the initial status of Task.B:
status = TaskStatus.InProgress
Type mismatch.
Required: TaskStatus<TaskResult.TaskBResult>
Found: TaskStatus.InProgress
Does anyone know how to change the data model so I'm allowed to run this (or a very similar) main function?
This could work with a very little change: just make TaskStatus a covariant generic class and make InProgress a TaskStatus<Nothing>. This is a typical strategy you can use when you have "special case" objects that represent no state. After this change, your code should compile:
sealed class Task<R : TaskResult> {
abstract val status: TaskStatus<R>
data class A(
val data: String,
override val status: TaskStatus<TaskResult.NoResult>,
) : Task<TaskResult.NoResult>()
data class B(
val data: String,
override val status: TaskStatus<TaskResult.TaskBResult>,
) : Task<TaskResult.TaskBResult>()
}
sealed class TaskStatus<out R : TaskResult> {
object InProgress : TaskStatus<Nothing>()
data class Completed<R : TaskResult>(val result: R) : TaskStatus<R>()
}
sealed class TaskResult {
object NoResult : TaskResult()
data class TaskBResult(val resultData: String) : TaskResult()
}
fun main() {
val taskA = Task.A(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = NoResult
)
)
val taskB = Task.B(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = TaskBResult(
resultData = "resultData"
)
)
)
}
I'm trying to use a resource string inside a companion object that is inside a data class. but I don't Know how to obtain context in that case in Kotlin.
Anyone knows how to do it?
data class PhoneCall(
val type: String,
val code: String,
val description: String){
companion object{
const val SOLUTION_NO_SOLUTION = "NO_SOLUTION"
const val SOLUTION_TOMORROW = "71_INAT"
const val SOLUTION_TODAY = "72_INAT"
val solutions = listOf(
PhoneCall(Service.Traffic.PICK_UP, SOLUTION_NO_SOLUTION, Resources.getSystem().getString(R.string.makeService))
)
}
I need to use a resource string in the 3 parameter, but I'm not able to get the context.
You can modify you PhoneCall model to store a string resource id instead of the actual string.
data class PhoneCall(
val type: String,
val code: String,
#StringRes val description: Int
) {
companion object {
const val SOLUTION_NO_SOLUTION = "NO_SOLUTION"
const val SOLUTION_TOMORROW = "71_INAT"
const val SOLUTION_TODAY = "72_INAT"
val solutions = listOf(
PhoneCall(Service.Traffic.PICK_UP, SOLUTION_NO_SOLUTION, R.string.makeService)
)
}
}
Then, when you need to display this data in the UI (say a TextView), you can fetch the string from the resource id.
descriptionTextView.text = getString(phoneCall.description)
I'm using kotlin and ObjectBox in my application. My object box entity looks something like
#Entity
class Order {
#Id var id: Long = 0
lateinit var customer: ToOne<Customer>
}
#Entity
class Customer {
#Id var id: Long = 0
#Backlink
lateinit var orders: List<Order>
}
But when I use #Parcelize, the properties are being ignored in the parcel. How do I use #Parcelize but still include these properties? I tried overriding writeToParcel but I am not allowed to override it due to #Parcelize.
According to docs, you have to declare all properties in primary constructor, which should be serialized via #Parcelize. All other ones are ignored.
ObjectBox doesn't support ToOne so you have to write custom Parceler. In the end your solution should look like this:
#Entity
#Parcelize
#TypeParceler<ToOne<Customer>, ToOneCustomerParceler>
class Order(
#Id var id: Long = 0,
var customer: ToOne<Customer>
) : Parcelable
#Entity
#Parcelize
class Customer(
#Id var id: Long = 0,
#Backlink var orders: List<Order>
) : Parcelable
object ToOneCustomerParceler : Parceler<ToOne<Customer>> {
override fun create(parcel: Parcel): ToOne<Customer> {
//Somehow recreate ToOne instance
...
}
override fun ToOne<Customer>.write(parcel: Parcel, flags: Int) {
val customer = target
...
}
}
Also don't forget to include correct dependencies:
dependencies {
compile "io.objectbox:objectbox-android:$objectboxVersion"
compile "io.objectbox:objectbox-kotlin:$objectboxVersion"
}
P.S. Use different models for each purpose (#Entity and #Parcelize) even if both are the same. It is much easier to manage them since you separate your intentions into 2 models, rather than trying to push everything into single one.
Let's take the class of a data class:
data class User(
val userNumber: Int = -1,
val name: String,
val userGroups; List<String> = emptyList(),
val screenName: String = "new-user"
)
When calling this function from Kotlin, it is pretty straightforward. I can simply use the named-argument syntax to do so. Calling from Java, I have to specify all values, or use the #JvmOverloads annotation, which generates the following constructors (in addition to the constructor that kotlin generates with the bit-mask for default values):
User(int userNumber, #NotNull String name, #NotNull List userGroups,
#NotNull String screenName)
User(int userNumber, #NotNull String name, #NotNull List userGroups)
User(int userNumber, #NotNull String name)
User(#NotNull String name)
Now, if I want to create a User object in Java equivalent to User(name="John Doe", userGroups=listOf("admin", "super") I can't do it with the above constructors. I CAN however do it if I put val userNumber: Int = -1 at the end in the data class declaration (the generation of constructors seems to depend on the order the optional arguments are defined in). Which is fine, because expecting kotlin to generate all permutations is going to heavily bloat some classes.
The biggest problem that tools like Jackson simply don't work as they have no idea which constructor to use (and not like I can annotate one of the generated ones specially).
So, is there a way to generate a (single) constructor like:
User(Integer userNumber, String name, List<String> userGroups, String screenName) {
this.userNumber = (userNumber == null) ? -1 : userNumber;
this.userGroups = (userGroups == null) ? Collections.emptyList() : userGroups;
//...
}
Currently I am using the above approach, but manually defining the constructors where I need them.
EDIT
I should clarify, creating a similar constructor doesn't work, obviously because both the signatures would clash on the JVM. This is what it would like in my case:
data class User(
val userNumber: Int = -1,
val name: String,
val userGroups; List<String> = emptyList(),
val screenName: String = "new-user"
) {
companion object {
#JvmStatic
#JsonCreator
fun constructionSupport(
#JsonProperty("userNumber") userNumber : Int?,
#JsonProperty("name") name : String,
#JsonProperty("userGroups") userGroups : List<String>?,
#JsonProperty("screenName") screenName : String?
) = User(
userNumber = userNumber ?: -1,
name = name,
userGroups = userGroups ?: emptyList(),
screenName = screenName ?: "new-user"
)
}
}
Also note the redundancy where I have to write the default values for the properties twice. I Now that I look at it, I doubt there exists a solution for this. Maybe this is a good use-case for a kapt based side-project of mine :)
Better solution is to add possibility to library understand Kotlin functional. For example, for Jackson exists jackson-module-kotlin. With this library we can use default arguments in data classes.
Example:
data class User(
val userNumber: Int = -1,
val name: String,
val userGroups: List<String> = emptyList(),
val screenName: String = "new-user"
)
fun main(args: Array<String>) {
val objectMapper = ObjectMapper()
.registerModule(KotlinModule())
val testUser = User(userNumber = 5, name = "someName")
val stringUser = objectMapper.writeValueAsString(testUser)
println(stringUser)
val parsedUser = objectMapper.readValue<User>(stringUser)
println(parsedUser)
assert(testUser == parsedUser) {
println("something goes wrong")
}
}
After kicking this around for a minute, I think I found a solution that may work well here. Simply define a top level function in the same source file, that will build the object. Perhaps like so:
fun build_user(userNumber: Int?, name: String, userGroups: List<String>?, screenName: String?) : User {
return User(if(userNumber !== null) userNumber else -1, name, if(userGroups !== null) userGroups else emptyList(),
if(screenName !== null) screenName else "new-user")
}
Then when you need it, you simply call it from Java:
User user = UserKt.build_user(null, "Hello", null, "Porterhouse Steak");
System.out.println(user);
Output from the example:
User(userNumber=-1, name=Hello, userGroups=[], screenName=Porterhouse Steak)
The method is somewhere between a constructor and a builder. It beats hammering out a full-blown Builder object, and avoids cluttering your data class with unnecessary Java-interop glue code messiness.
See Package Level Functions for more information.