Search code examples
kotlinclassumlgetter-setterclass-diagram

Which UML class diagram is more accurate for Kotlin class attributes?


I want to create a UML class diagram to represent a Kotlin class. I have come across a few options and came up with two alternatives. However, I'm unsure which of the two is the most appropriate and accurate. Can someone help me determine which UML diagram is correct or better for this Kotlin class?

Here is the Kotlin class I want to represent:

class Student(val name: String, var address: String) {

    private var numCourses: Int = 0

    private val courses: Array<String> = Array(MAX_COURSES) { "" }

    private val grades: IntArray = IntArray(MAX_COURSES)
    companion object {
        const val MAX_COURSES = 30
    }

    fun printGrades() {
        print(name)
        for (i in 0 until numCourses) {
            print(" ${courses[i]}:${grades[i]}, ")
        }
        println()
    }

    fun calculateAverageGrade() = grades.sum().toDouble() / grades.size

    fun addCourseGrade(course: String, grade: Int) {

        require(numCourses < MAX_COURSES) {
            "A student cannot take more than $MAX_COURSES courses"
        }

        require(grade in 0..100) {
            "Grade must be between 0 and 100"
        }

        courses[numCourses] = course
        grades[numCourses] = grade
        numCourses++
    }

    override fun toString() = "name($address)"

}

Option 1: With getters and setters

All the class' attributes are private and the public ones get public getters/setters:

Uml 1

Option 2: using some additional UML notations

I used these two answers on Stackoverflow for the <<get/set>> annotations (see here) and {readOnly} attribute (see here).

Uml 2


Solution

  • Let's keep it simple. I read that Kotlin properties are mutable when defined with var and read only when defined with val. I see in your code two public and three private properties. Looking at the UML definition of a property and UML rules for accessibility, this translates very simply to:

    + name: String {readOnly}
    + address: String 
    - numCourses: Int=0
    - courses: String[MAX_COURSES] 
    - grades: Int[MAX_COURSES] 
    

    Some more explanations:

    • There is no need to add anything about getters and setters here, if you don't implement getters and setters explicitly. The question you were referring to was for a design where all properties are kept private and are exposed via explicit getters and setters. You don't do this, so you don't need it.
    • In your code you don't define the array to be of size of magic number 30. So keep this good practice also in UML and use a multiplicity of MAX_COURSES which is a constant value.
    • there is an important semantic difference between Kotlin arrays and UML multiplicities. In Kotlin, the array is read only, because one array is created, and this array object of fixed size is not modified. However, you can still modify the objects it contains. In UML there is no array; the multiplicity tell that there are MAX_COURSES properties (without details of how this is implemented). If you would use {readOnly}, it would apply to the all the MAX_COURSES elements, meaning that you could only define their values once and not change them afterwards (i.e. stay forever ""): this is not what you want to express.

    Additional remarks:

    • For a Kotlin property xyz with getters and setters using a backing property, you do not need anything else than the standard UML notation of derived property: /xyz. UML specifications explain that:

      But where a derived Property is changeable, an implementation is expected to make appropriate changes to the model in order for all the constraints to be met, in particular the derivation constraint for the derived Property. The derivation for a derived Property may be specified by a constraint.

    • If you would define in your class explicit getters and setters and backing field, you could use «get», «set» stereotypes to inform that there could be some operations behind the scene. For properties in interfaces, I'd do it systematically, to clarify what the implementing classes have to provide.