Function Literals with Receiver
We have discussed this topic before, reader who is unfamiliar with that concept can have a brief look at Function Literals with Receiver.
Code Separation
Original Bulky StudentPackage
Class
StudentPackage
ClassThis file is very huge, mixing the joined entities, the domain behaviours, the domain behaviour validations and also factory methods.
Code Separation Strategy
We separate the group of logic into the following classes:
StudentPackageAction
StudentPackageValidation
StudentPackageFactory
We use function literals with StudentPackage
as a receiver to provide an additional scope in which methods in C
are available, where C = Student{Action, Validation, Vactory}
.
Example of resulting code style of calling domain behaviour
@Transactional fun duplicateClasses(reqBody: DuplicateClassRequest) { val (classId, numberOfWeeks) = reqBody val existingClass = classRepository.findByIdOrNull(classId) ?: throw Exception("Class not found") val studentPackage = studentPackageRepository.findByIdOrNull(existingClass.studentPackage?.id!!) ?: throw TimetableException("Student package not found") studentPackage.action { addDuplicatedClasses(classId, numberOfWeeks) updateOfficialEndDateFromLastClass() resetClassNumbers() } studentPackageRepository.save(studentPackage) }
In the code snippet above we have extracted
addDuplicatedClasses
updateOfficialEndDateFromLastClass
resetClassNumbers
into StudentPackageAction
.
The lambda function accepted by studentPackage.action
can access the methods in StudentPackageAction
Code Separation by Function Literals with StudentPackage as Receiver
Separated File Structure

StudentPackage
Class (reduced into standard Entity class)
StudentPackage
Class (reduced into standard Entity class)package dev.james.alicetimetable.commons.database.entities import dev.james.alicetimetable.commons.database.enums.Classroom import dev.james.processor.GenerateDTO import jakarta.persistence.* import jakarta.persistence.Table import org.hibernate.annotations.* import org.hibernate.annotations.CascadeType import org.hibernate.dialect.PostgreSQLEnumJdbcType import org.springframework.data.domain.AbstractAggregateRoot import java.util.* /** * This class is generated by jOOQ. */ @Suppress("UNCHECKED_CAST") @Entity @GenerateDTO @DynamicInsert @Table( name = "student_package", schema = "public" ) class StudentPackage( @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Int? = null, @Column(name = "start_date", nullable = false) var startDate: Double, @Column(name = "paid_at") var paidAt: Double? = null, @Column(name = "official_end_date") var officialEndDate: Double? = null, @Column(name = "expiry_date") var expiryDate: Double? = null, @Column(name = "min", nullable = false) var min: Int, @Column(name = "course_id", nullable = false) var courseId: Int, @Column(name = "created_at") @Generated var createdAt: Double? = null, @Column(name = "created_at_hk") @Generated var createdAtHk: String? = null, @Column(name = "num_of_classes") var numOfClasses: Int, @Column(name = "default_classroom") @Enumerated(EnumType.STRING) @JdbcType(PostgreSQLEnumJdbcType::class) var defaultClassroom: Classroom? = null, @Column(name = "uuid") @Generated var uuid: UUID? = null, ) : AbstractAggregateRoot<StudentPackage>() { @ManyToOne @JoinColumn(name = "course_id", updatable = false, insertable = false) var course: Course? = null @ManyToOne @JoinTable( name = "rel_student_studentpackage", joinColumns = [JoinColumn(name = "student_package_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "student_id", referencedColumnName = "id")] ) var student: Student? = null @OneToMany @Cascade(CascadeType.ALL) @JoinTable( name = "rel_class_studentpackage", joinColumns = [JoinColumn(name = "student_package_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "class_id", referencedColumnName = "id")] ) var classes: MutableSet<Class> = mutableSetOf() fun <T> registerEvent_(event: T & Any): T { return registerEvent(event) } }
StudentPackageAction
Class
StudentPackageAction
Class// StudentPackageAction.kt fun <T> StudentPackage.action(block: StudentPackageAction.() -> T): T { val scope = StudentPackageAction(this) return scope.block() } class StudentPackageAction(private val pkg: StudentPackage) { fun updateOfficialEndDateFromLastClass() { pkg.officialEndDate = pkg.classes.lastOrNull()?.dayUnixTimestamp } fun markAsPaid() { pkg.paidAt = DateTime().millis.toDouble() pkg.registerEvent_(TimetableEvents.PackagePaidMarked(pkg.id!!)) } fun calculateNewEndDate() { val lastClass = pkg.classes.sortedBy { it.hourUnixTimestamp }.lastOrNull() if (lastClass != null) { pkg.officialEndDate = DateTime(lastClass.hourUnixTimestamp.toLong()).plusMinutes(lastClass.min).millis.toDouble() } } fun markAsUnpaid() { pkg.paidAt = null pkg.registerEvent_(TimetableEvents.PackageUnpaidMarked(pkg.id!!)) } fun resetClassNumbers() { pkg.registerEvent_(TimetableEvents.PackageResetClassNumbersRequested(pkg.id!!)) } fun addNextPackage() { pkg.registerEvent_(TimetableEvents.AddNextPackageRequested(pkg.id!!)) } fun addClasses(newClasses: List<Class>) { pkg.withValidation { validation.`should not create classes that are in the past`(newClasses) validation.`new classes should not have intersection with existing classes`(newClasses) validation.`new classes total mins should not exceed total minutes of remaining classes of a package`(newClasses) } addAndRemoveRedundantClasses(newClasses) } fun deleteOneClass(classId: Int) { removeSingleClass(classId) pkg.registerEvent_(TimetableEvents.SingleClassDeleted(classId)) } fun addAndRemoveRedundantClasses(classes: List<Class>) { val allClasses = (pkg.classes.toMutableList() + classes).sortedBy { it.hourUnixTimestamp } val packageTotalMin = pkg.min*pkg.numOfClasses val finalizedClasses = mutableSetOf<Class>() var minCounter = 0 allClasses.forEach { minCounter += it.min if (minCounter <= packageTotalMin) { finalizedClasses.add(it) } else { return@forEach } } pkg.classes = finalizedClasses } fun deleteGroupedClassesOfClass(classId: Int) { val cls = pkg.classes.find { it.id == classId } if (cls == null) { throw TimetableException("class cannot be found") } if (cls.group == null) { removeSingleClass(classId) } else { removeRemainingClass(classId, cls) } pkg.registerEvent_(TimetableEvents.GroupOfClassesDeleted(classId = classId)) } fun removeRemainingClass(classId: Int, cls: Class) { val remainingClasses = pkg.classes.filter { it.hourUnixTimestamp >= cls.hourUnixTimestamp } pkg.classes.removeAll { it.id in remainingClasses.map { it.id } } } fun removeSingleClass(classId: Int) { pkg.classes.removeAll { it.id == classId } } fun addDuplicatedClasses(classId: Int, numberOfWeeks: Int) { pkg.withValidation { validation.`class must exist within package`(classId) validation.`the number of weeks to duplicate must be at least 2`(numberOfWeeks) } val cls = pkg.classes.find { it.id == classId } ?: throw TimetableException("no class can be found for $classId") val targetClassHrTimestamp = DateTime(cls.hourUnixTimestamp.toLong()) val targetClassDayTimestamp = DateTime(cls.dayUnixTimestamp.toLong()) val newClasses = (0 until numberOfWeeks).map { offset -> val classHrTimestamp = targetClassHrTimestamp.plusWeeks(1 + offset).millis.toDouble() val classDayTimestamp = targetClassDayTimestamp.plusWeeks(1 + offset).millis.toDouble() Class(dayUnixTimestamp = classDayTimestamp, hourUnixTimestamp = classHrTimestamp, min = cls.min, actualClassroom = cls.actualClassroom) } this.addClasses(newClasses) pkg.registerEvent_(TimetableEvents.ClassDuplicatedEvent(classId = classId, numOfWeeks = numberOfWeeks)) } fun updateClassBasicInfo(classId: Int, basicInfo: UpdateClassRequest) { val targetClass = pkg.classes.find { it.id == classId } ?: throw TimetableException("no class found for $classId") val (_, min, class_status, remark, actual_classroom) = basicInfo targetClass.min = min targetClass.classStatus = class_status targetClass.remark = remark targetClass.actualClassroom = actual_classroom pkg.withValidation { validation.`target classes (possibly from other package) should not have time conflict with current package`(listOf(targetClass)) validation.`modified minutes should not make the whole package exceed pre-defined total`() } pkg.registerEvent_(TimetableEvents.ClassInfoUpdated(reqBody = basicInfo)) } fun update(reqBody: UpdatePackageRequest) { val (id1, num_of_classes, start_date, official_end_date, min1, course_id, default_classroom) = reqBody this.apply { pkg.numOfClasses = num_of_classes pkg.startDate = start_date.toDouble() pkg.officialEndDate = official_end_date.toDouble() pkg.min = min1 pkg.courseId = course_id pkg.defaultClassroom = default_classroom } pkg.registerEvent_(TimetableEvents.PackageUpdated(reqBody)) } fun registerCreatedEvent() { pkg.registerEvent_(TimetableEvents.PackageCreatedEvent(pkg.toDTO())) } }
StduentPackageValidation
Class
StduentPackageValidation
Class// StduentPackageValidation package dev.james.alicetimetable.commons.database.validations.aggregate import dev.james.alicetimetable.commons.database.entities.Class import dev.james.alicetimetable.commons.database.entities.StudentPackage import dev.james.alicetimetable.commons.exception.TimetableException import dev.james.alicetimetable.commons.utilityclass.Interval import org.joda.time.DateTime fun <T> StudentPackage.withValidation(block: StudentPackageValidation.() -> T): T { val scope = StudentPackageValidation(this) return scope.block() } class StudentPackageValidation(private val pkg: StudentPackage) { val validation = Validation() inner class Validation { fun `should not create classes that are in the past`(newClasses: List<Class>) { val current = DateTime().millis.toDouble() newClasses.forEach { if (it.hourUnixTimestamp < current) { throw TimetableException("Only classes in the future can be created.") } } } fun `modified minutes should not make the whole package exceed pre-defined total`() { val newTotal = pkg.classes.map { it.min }.sum() val predefinedTotal = pkg.min*pkg.numOfClasses if (newTotal > predefinedTotal) { throw TimetableException("modified minutes should not make the whole package exceed pre-defined total") } } fun `new classes total mins should not exceed total minutes of remaining classes of a package`(classes: List<Class>) { val currMillis = DateTime().millis.toDouble() val classesAttended = classes.filter { it.hourUnixTimestamp <= currMillis } val packageTotalMins = pkg.min*pkg.numOfClasses val consumedMins = classesAttended.map { it.min }.sum() val remainingMins = packageTotalMins - consumedMins val newClassesTotalMins = classes.map { it.min }.sum() if (newClassesTotalMins > remainingMins) { throw TimetableException("Class creation failed, requested (${newClassesTotalMins}mins) is more than available (${remainingMins}mins) in a package") } } fun `target classes (possibly from other package) should not have time conflict with current package`(classesToMove: Iterable<Class>) { val classIdsToMove = classesToMove.map { it.id } val classesOfConcern = pkg.classes.filter { it.id !in classIdsToMove } val hasIntersection = intersectionsOfTwoSetOfClasses(currClasses = classesOfConcern, newClasses = classesToMove) if (hasIntersection) { throw TimetableException("New classes have time conflict with existing classes") } } fun `new classes should not have intersection with existing classes`(newclasses: List<Class>) { val hasIntersection = intersectionsOfTwoSetOfClasses(currClasses = pkg.classes, newClasses = newclasses) if (hasIntersection) { throw TimetableException("New classes have time conflict with existing classes") } } fun `class must exist within package`(classId: Int) { val cls = pkg.classes.find { it.id == classId } if (cls == null) { throw TimetableException("Target class does not exist") } } fun `the number of weeks to duplicate must be at least 2`(numberOfWeeks: Int) { if (numberOfWeeks <= 1) { throw TimetableException("the number of weeks to duplicate must be at least 2") } } fun validateDuplicateClassRequest(cls: Class, numOfWeeks: Int) { val availableMins = pkg.min*pkg.numOfClasses val consumedMins = pkg.classes.map { it.min }.sum() val extraMinsRequested = cls.min*(numOfWeeks - 1) val remainingMins = availableMins - consumedMins val isValidRequest = remainingMins - extraMinsRequested >= 0 if (!isValidRequest) { throw TimetableException("Remaining mins: $remainingMins, but $extraMinsRequested extra mins is requested") } if (numOfWeeks <= 1) { throw TimetableException("You should duplicate to at least 2 weeks") } if (cls.group != null) { throw TimetableException("Please detach the class from its parent group before duplicating it.") } } fun intersectionsOfTwoSetOfClasses(currClasses: Iterable<Class>, newClasses: Iterable<Class>): Boolean { val existingClassIntervals = currClasses.map { it.getInterval() } val newClassIntervals = newClasses.map { it.getInterval() } val intersectedIntervals = mutableSetOf<Interval>() for (oldInterval in existingClassIntervals) { for (newInterval in newClassIntervals) { val interval = oldInterval.intersect(newInterval) interval?.let { intersectedIntervals.add(interval) } } } val hasIntersection = intersectedIntervals.isNotEmpty() return hasIntersection } } }
The factory methods
Unlike the previous examples, factory method usually has no instantiated instance to stick with (as they are static). We create a placeholder to work with extension function (an empty Factory
class):
Empty Factory placeholder class
package dev.james.alicetimetable.commons.database.factories class Factory
We will be using extension function to provide new scope to access factory methods (does it taste very Golang?).
Example of resulting code style
@Transactional fun createClass(reqBody: CreateClassRequest) { val (numOfClasses, dayUnixTimestamp, hourUnixTimestamp, min, studentPackageId, actualClassroom) = reqBody val targetPackage = studentPackageRepository.findByIdOrNull(studentPackageId) ?: throw Exception("Student package not found") val factory = Factory() val classes = factory.studentPackage { createRecurringClasses(startDay = DateTime(dayUnixTimestamp.toLong()), startHour = DateTime(hourUnixTimestamp.toLong()), numOfClasses = numOfClasses, min = min, classroom = actualClassroom) } targetPackage.action { addClasses(classes) calculateNewEndDate() resetClassNumbers() } studentPackageRepository.save(targetPackage) }
Factory implementation file
package dev.james.alicetimetable.commons.database.factories import dev.james.alicetimetable.commons.database.actions.action import dev.james.alicetimetable.commons.database.entities.Class import dev.james.alicetimetable.commons.database.entities.StudentPackage import dev.james.alicetimetable.commons.database.enums.Classroom import org.joda.time.DateTime fun <T> Factory.studentPackage(block: StudentPackageFactory.() -> T): T { val scope = StudentPackageFactory() return scope.block() } class StudentPackageFactory() { fun create(start_date: Long, min: Int, course_id: Int, num_of_classes: Int, default_classroom: Classroom, startDay: DateTime ): StudentPackage { val pkg = StudentPackage(startDate = start_date.toDouble(), min = min, expiryDate = startDay.plusMonths(4).millis.toDouble(), courseId = course_id, numOfClasses = num_of_classes, defaultClassroom = default_classroom) // will be triggered once this enetity is saved pkg.action { registerCreatedEvent() } return pkg } fun createRecurringClasses( startDay: DateTime, startHour: DateTime, numOfClasses: Int, min: Int, classroom: Classroom ): List<Class> { return (0 until numOfClasses).map { week -> val nextStartDay = startDay.plusWeeks(week) val nextStartHour = startHour.plusWeeks(week) Class( dayUnixTimestamp = nextStartDay.millis.toDouble(), hourUnixTimestamp = nextStartHour.millis.toDouble(), min = min, actualClassroom = classroom ) } } }