1
\$\begingroup\$

As so far I've been mostly developing existing products and doing maintenance/engineering I feel in need to model something from time to time for fun and for self educational purposes.

Attached code is a simple domain model of workflow system, like Jira (I admit I was partially influenced by this particular model).

It is very basic model of course. It does not support many concepts that I would like to see in this kind of software, however as I mentioned before it is something like little exercise.

Let's start from the Workflow itself. Workflow contains definition of task (what fields should it have), possible states and their transitions, and groups (of tasks).

@Document
data class Workflow(
    val id: String = generateId(),
    val name: String,
    val taskTemplate: TaskTemplate,
    val stateTransitions: StateTransitions = StateTransitions.basic(),
    val rootTaskGroup: TaskGroup = TaskGroup.root())

This is how the task template looks like:

 data class TaskTemplate(val taskPropertyDefinitions: List<TaskPropertyDefinition> = listOf()) {

    val taskPropertyDefinitionsMap = taskPropertyDefinitions.map { it.name to it }.toMap()

    companion object {
        fun basic(): TaskTemplate {
            return TaskTemplate(listOf(
                    TaskPropertyDefinition("title", StringTaskProperty::class),
                    TaskPropertyDefinition("description", StringTaskProperty::class)
            ))
        }
    }

    fun addPropertyDefinition(taskPropertyDefinition: TaskPropertyDefinition): TaskTemplate {
        return this.copy(taskPropertyDefinitions = this.taskPropertyDefinitions + taskPropertyDefinition)
    }

    fun getTaskPropertyDefinition(propertyDefinitionName: String) = taskPropertyDefinitionsMap[propertyDefinitionName]

    fun propertyIsValid(taskProperty: TaskProperty): Boolean {
        return getTaskPropertyDefinition(taskProperty.propertyName)?.let { definition ->
            taskProperty.matchesDefinition(definition)
        } ?: false
    }
}


data class TaskPropertyDefinition(val name: String,
                                  val propertyType: KClass<out TaskProperty>,
                                  val optional: Boolean = false)

abstract class TaskProperty(val propertyName: String) {

    abstract fun matchesDefinition(taskPropertyDefinition: TaskPropertyDefinition): Boolean

}

data class StringTaskProperty(val name: String, val value: String?) : TaskProperty(name) {

    override fun matchesDefinition(taskPropertyDefinition: TaskPropertyDefinition): Boolean {
        return if (!taskPropertyDefinition.optional) {
            value != null
        } else true
    }
}

data class MultiStringTaskProperty(val name: String, val values: List<String> = listOf()) : TaskProperty(name) {

    override fun matchesDefinition(taskPropertyDefinition: TaskPropertyDefinition): Boolean {
        return if (!taskPropertyDefinition.optional) {
            values.isNotEmpty()
        } else true
    }
}

...etc - there are possibly many different task properties, like check-list, some selects, multiselects.

State transitions:

data class StateTransitions(val initialState: State, val stateTransitions: List<StateTransition>) : Valid {

    override fun isValid(): Boolean {
        return true
    }

    companion object {
        fun basic(): StateTransitions {
            return StateTransitions(State("New"),
                    listOf(StateTransition.oneToOne((State("New")), State("Done"))))
        }
    }

    fun transitionAllowed(sourceState: State, destinationState: State): Boolean {
        stateTransitions.find { it.sourceState == sourceState }?.let { transition ->
            if (transition.allowedDestinations.contains(destinationState)) return true
        }
        return false
    }
}

data class StateTransition(val sourceState: State, val allowedDestinations: List<State>) {
    companion object {
        fun oneToOne(sourceState: State, destinationState: State): StateTransition {
            return StateTransition(sourceState, listOf(destinationState))
        }
    }
}

data class State(val name: String)

Now stuff related to task itself:

@Document
data class Task(@Id val id: String = generateId(),
                @DBRef val workflow: Workflow,
                val taskGroups: Set<TaskGroup> = setOf(workflow.rootTaskGroup),
                val taskHistory: TaskHistory = TaskHistory.start(),
                val state: State = workflow.stateTransitions.initialState,
                val taskProperties: Map<String, TaskProperty> = mapOf(),
                val published: Boolean = false) {

    fun applyAction(action: TaskAction): Task {
        return when (action) {
            is AddToGroupsAction -> addToGroupsAction(action)
            is PublishAction -> publishAction(action)
            is StateChangeAction -> stateChangeAction(action)
            is UpdatePropertiesAction -> updatePropertiesAction(action)
            else -> throw UnrecognizedActionException(action, this.id)
        }
    }

    private fun stateChangeAction(action: StateChangeAction): Task {
        return if (workflow.stateTransitions.transitionAllowed(this.state, action.newState))
            this.copy(state = action.newState, taskHistory = this.taskHistory.action(action))
        else throw DisallowedStateTransitionException(state, this.id)
    }

    private fun publishAction(action: PublishAction): Task {
        return this.copy(published = true, taskHistory = this.taskHistory.action(action))
    }

    private fun addToGroupsAction(action: AddToGroupsAction): Task {
        return this.copy(taskGroups = this.taskGroups + action.groups, taskHistory = this.taskHistory.action(action))
    }

    private fun updatePropertiesAction(action: UpdatePropertiesAction): Task {
        return this.copy(taskProperties = updateProperties(action.newProperties, workflow.taskTemplate),
                taskHistory = this.taskHistory.action(action))
    }

    private fun updateProperties(newProperties: List<TaskProperty>, taskTemplate: TaskTemplate): Map<String, TaskProperty> {
        return HashMap(taskProperties).also { properties ->
            newProperties.associateBy {
                it.propertyName
            }.forEach { propertyName, property ->
                if (taskTemplate.propertyIsValid(property)) {
                    properties += propertyName to property
                }
            }
        }
    }
}

data class TaskGroup(val name: String, val childGroups: List<TaskGroup> = listOf()) {

    companion object {
        fun root() = TaskGroup("root")
    }

    fun addChildGroup(taskGroup: TaskGroup): TaskGroup {
        return this.copy(childGroups = this.childGroups + taskGroup)
    }

    fun getChildGroup(wantedGroupName: String): TaskGroup? {
        fun nameMatches(childGroup: TaskGroup, groupName: String) = childGroup.name == groupName
        mutableStackOf(this).let { stack ->
            while (stack.isNotEmpty()) {
                val currentGroup = stack.remove()
                if (nameMatches(currentGroup, wantedGroupName)) return currentGroup
                currentGroup.childGroups.forEach { currentGroupChild ->
                    if (nameMatches(currentGroupChild, wantedGroupName)) return currentGroupChild
                    stack.addAll(currentGroupChild.childGroups)
                }
            }
            return null
        }
    }
}

data class TaskHistory(val taskActions: List<TaskAction> = listOf()) {
    companion object {
        fun start() : TaskHistory {
            return TaskHistory(listOf(CreatedTaskAction))
        }
    }

    fun action(action: TaskAction): TaskHistory {
        return this.copy(taskActions = this.taskActions + action)
    }
}

And actions:

abstract class TaskAction(val actionTime: LocalDateTime = LocalDateTime.now())
class AddToGroupsAction(val groups: List<TaskGroup>) : TaskAction()
object CreatedTaskAction : TaskAction()
object PublishAction : TaskAction()
data class StateChangeAction(val newState: State) : TaskAction()
data class UpdatePropertiesAction(val newProperties: List<TaskProperty>) : TaskAction()

And again - many actions are missing. Here I'm afraid if it will be a good idea to handle actions using "when" expression, when let's say we have 25-50 actions to deal with.

I'd be very grateful for any suggestions or opinions about this simple model.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.