Skip to content

Commit b749cc5

Browse files
authored
Merge pull request #5 from The-Self-Taught-Software-Engineer/idiomatic-kotlin/#02_collections-vs-sequences
Idiomatic Kotlin (#02) – Collections vs. Sequences
2 parents fa1f408 + 83dfaa2 commit b749cc5

11 files changed

Lines changed: 332 additions & 14 deletions

File tree

build.gradle.kts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ repositories {
1212
}
1313

1414
dependencies {
15-
implementation("io.github.microutils:kotlin-logging:2.0.6")
15+
implementation("ch.qos.logback:logback-classic:1.2.3")
16+
implementation("com.github.javafaker:javafaker:1.0.2")
1617
implementation("com.sun.mail:javax.mail:1.5.5")
18+
implementation("io.github.microutils:kotlin-logging:2.0.6")
19+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
20+
implementation("org.slf4j:slf4j-api:1.7.30")
1721

1822
testImplementation(kotlin("test"))
23+
testImplementation("io.mockk:mockk:1.12.0")
1924
}
2025

2126
tasks.test {

src/main/kotlin/codes/jakob/tstse/example/common/Developer.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,29 @@ data class Developer(
77
val name: Name,
88
val type: DeveloperType,
99
val emailAddress: String,
10-
var assigned: Boolean,
11-
)
10+
var assignment: Assignment?,
11+
) {
12+
val assigned: Boolean
13+
get() {
14+
return assignment != null
15+
}
16+
17+
override fun toString(): String {
18+
return "Developer(id=$id, name=$name, type=$type, emailAddress='$emailAddress', assigned=$assigned)"
19+
}
20+
21+
override fun equals(other: Any?): Boolean {
22+
if (this === other) return true
23+
if (javaClass != other?.javaClass) return false
24+
25+
other as Developer
26+
27+
if (id != other.id) return false
28+
29+
return true
30+
}
31+
32+
override fun hashCode(): Int {
33+
return id.hashCode()
34+
}
35+
}

src/main/kotlin/codes/jakob/tstse/example/common/Name.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,13 @@ package codes.jakob.tstse.example.common
33
data class Name(
44
val givenNames: List<String>,
55
val familyName: String,
6-
)
6+
) {
7+
constructor(fullName: String) : this(
8+
givenNames = fullName.substringBeforeLast(' ').split(' '),
9+
familyName = fullName.substringAfterLast(' '),
10+
)
11+
12+
override fun toString(): String {
13+
return "${givenNames.joinToString(" ")} $familyName"
14+
}
15+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
@file:Suppress("RedundantSuspendModifier", "unused")
2+
3+
package codes.jakob.tstse.example.idiomatic.collections_vs_sequences
4+
5+
import codes.jakob.tstse.example.common.Developer
6+
import codes.jakob.tstse.example.common.DeveloperType
7+
import codes.jakob.tstse.example.common.Notification
8+
import codes.jakob.tstse.example.idiomatic.collections_vs_sequences.shared.DeveloperNotificationService
9+
import codes.jakob.tstse.example.idiomatic.scopefunctions.DeveloperRepository
10+
import mu.KLogger
11+
import mu.KotlinLogging
12+
import java.time.Clock
13+
import javax.mail.Session
14+
15+
class CollectionsDeveloperNotificationService(
16+
emailSession: Session,
17+
clock: Clock,
18+
private val repository: DeveloperRepository,
19+
) : DeveloperNotificationService(emailSession, clock) {
20+
override suspend fun notifyDevelopersByEmail(developerType: DeveloperType, notification: Notification) {
21+
var operation = 0
22+
23+
val developers: Set<Developer> = repository.findDevelopersByType(developerType)
24+
25+
val unassignedDevelopers = developers
26+
.filterNot { dev ->
27+
dev.assigned
28+
.also { logger.debug("${++operation}. Filtered: ${dev.emailAddress to dev.assigned}") }
29+
}
30+
val emails = unassignedDevelopers
31+
.map { dev ->
32+
generateEmail(dev, notification)
33+
.also { logger.debug("${++operation}. Mapped: ${dev.emailAddress to dev.assigned}") }
34+
}
35+
.take(unassignedDevelopers.count() / 2)
36+
37+
emails
38+
.forEach { email ->
39+
sendEmail(email)
40+
.also { logger.debug("${++operation}. Sending email to ${email.firstRecipient()}") }
41+
}
42+
}
43+
44+
companion object {
45+
private val logger: KLogger = KotlinLogging.logger {}
46+
}
47+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@file:Suppress("RedundantSuspendModifier", "unused")
2+
3+
package codes.jakob.tstse.example.idiomatic.collections_vs_sequences
4+
5+
import codes.jakob.tstse.example.common.Developer
6+
import codes.jakob.tstse.example.common.DeveloperType
7+
import codes.jakob.tstse.example.common.Notification
8+
import codes.jakob.tstse.example.idiomatic.collections_vs_sequences.shared.DeveloperNotificationService
9+
import codes.jakob.tstse.example.idiomatic.collections_vs_sequences.shared.SpyCollection.Companion.toSpyCollection
10+
import codes.jakob.tstse.example.idiomatic.scopefunctions.DeveloperRepository
11+
import mu.KLogger
12+
import mu.KotlinLogging
13+
import java.time.Clock
14+
import javax.mail.Session
15+
16+
class SequencesDeveloperNotificationService(
17+
emailSession: Session,
18+
clock: Clock,
19+
private val repository: DeveloperRepository,
20+
) : DeveloperNotificationService(emailSession, clock) {
21+
override suspend fun notifyDevelopersByEmail(developerType: DeveloperType, notification: Notification) {
22+
var operation = 0
23+
24+
val developers: Set<Developer> = repository.findDevelopersByType(developerType)
25+
26+
val unassignedDevelopers = developers
27+
.asSequence()
28+
.filterNot { dev ->
29+
dev.assigned
30+
.also { logger.debug("${++operation}. Filtered: ${dev.emailAddress to dev.assigned}") }
31+
}
32+
val emails = unassignedDevelopers
33+
.map { dev ->
34+
generateEmail(dev, notification)
35+
.also { logger.debug("${++operation}. Mapped: ${dev.emailAddress to dev.assigned}") }
36+
}
37+
.take(unassignedDevelopers.count() / 2)
38+
.toSpyCollection()
39+
40+
emails
41+
.forEach { email ->
42+
sendEmail(email)
43+
.also { logger.debug("${++operation}. Sending email to ${email.firstRecipient()}") }
44+
}
45+
}
46+
47+
companion object {
48+
private val logger: KLogger = KotlinLogging.logger {}
49+
}
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@file:Suppress("RedundantSuspendModifier", "unused")
2+
3+
package codes.jakob.tstse.example.idiomatic.collections_vs_sequences.shared
4+
5+
import codes.jakob.tstse.example.common.Developer
6+
import codes.jakob.tstse.example.common.DeveloperType
7+
import codes.jakob.tstse.example.common.Notification
8+
import java.time.Clock
9+
import java.util.*
10+
import javax.mail.Message
11+
import javax.mail.Session
12+
import javax.mail.internet.InternetAddress
13+
import javax.mail.internet.MimeMessage
14+
15+
abstract class DeveloperNotificationService(
16+
private val emailSession: Session,
17+
private val clock: Clock,
18+
) {
19+
abstract suspend fun notifyDevelopersByEmail(developerType: DeveloperType, notification: Notification)
20+
21+
protected fun generateEmail(developer: Developer, notification: Notification): Message {
22+
return MimeMessage(emailSession).apply {
23+
setRecipients(Message.RecipientType.TO, InternetAddress.parse(developer.emailAddress, false))
24+
replyTo = InternetAddress.parse(REPLY_TO_ADDRESS, false)
25+
subject = generateEmailSubject(developer, notification)
26+
setText(generateEmailBody(developer, notification))
27+
}
28+
}
29+
30+
protected suspend fun sendEmail(email: Message) {
31+
email.apply {
32+
sentDate = Date.from(clock.instant())
33+
}
34+
}
35+
36+
private fun generateEmailSubject(developer: Developer, notification: Notification): String {
37+
return notification.info
38+
}
39+
40+
private fun generateEmailBody(developer: Developer, notification: Notification): String {
41+
return "Dear ${developer.name}"
42+
}
43+
44+
companion object {
45+
private const val REPLY_TO_ADDRESS = "noreply@example.com"
46+
47+
@JvmStatic
48+
protected fun Message.firstRecipient(): String = allRecipients.map { it.toString() }.first()
49+
}
50+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package codes.jakob.tstse.example.idiomatic.collections_vs_sequences.shared
2+
3+
import mu.KLogger
4+
import mu.KotlinLogging
5+
6+
/**
7+
* A [MutableCollection] that logs whenever it performs an action.
8+
* Note that this class should only be used for debugging purposes.
9+
*/
10+
class SpyCollection<E>(elements: Collection<E> = emptyList()) : AbstractMutableCollection<E>() {
11+
init {
12+
logger.debug { "Instantiated ${SpyCollection::class.simpleName} with ${elements.count()} elements" }
13+
}
14+
15+
override val size: Int get() = backing.size
16+
private val backing: MutableCollection<E> = ArrayList(elements)
17+
private var capacity = 10
18+
19+
override fun add(element: E): Boolean {
20+
if (size + 1 > capacity) {
21+
capacity += capacity / 2
22+
logger.debug { "Increased capacity to $capacity" }
23+
}
24+
return backing.add(element).also { logger.debug { "Added $element" } }
25+
}
26+
27+
override fun iterator(): MutableIterator<E> {
28+
return backing.iterator()
29+
}
30+
31+
companion object {
32+
private val logger: KLogger = KotlinLogging.logger {}
33+
34+
fun <E> Sequence<E>.toSpyCollection(): SpyCollection<E> {
35+
val destination = SpyCollection<E>()
36+
for (item in this) {
37+
destination.add(item)
38+
}
39+
return destination
40+
}
41+
42+
fun <E> Collection<E>.toSpyCollection(): SpyCollection<E> {
43+
return SpyCollection(this)
44+
}
45+
}
46+
}

src/main/kotlin/codes/jakob/tstse/example/idiomatic/scopefunctions/let.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ class AssignmentService2(private val developerRepository: DeveloperRepository) {
1414
): Notification {
1515
return developerRepository.findRandomDeveloperByType(developerType)
1616
?.let { developer ->
17-
developer.assigned = true
18-
developerRepository.save(developer)
19-
20-
Assignment(
17+
developer.assignment = Assignment(
2118
name = generateName(assignmentType),
2219
developers = setOf(developer),
2320
type = assignmentType,
2421
briefing = MAINTAINENANCE_DEFAULT_BRIEFING,
25-
).generateNotification()
22+
)
23+
developer.assignment!!.generateNotification().also {
24+
developerRepository.save(developer)
25+
}
2626
}
2727
?: error("No developer with type '$developerType' exists to assign for maintenance")
2828
}

src/main/kotlin/codes/jakob/tstse/example/idiomatic/scopefunctions/run.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ class AssignmentService1(private val developerRepository: DeveloperRepository) {
2020

2121
return developerRepository.findRandomDeveloperByType(developerType)
2222
?.run {
23-
assigned = true
24-
developerRepository.save(this)
25-
26-
Assignment(
23+
assignment = Assignment(
2724
name = generateName(assignmentType),
2825
developers = setOf(this),
2926
type = assignmentType,
3027
briefing = MAINTAINENANCE_DEFAULT_BRIEFING,
31-
).generateNotification()
28+
)
29+
assignment!!.generateNotification().also {
30+
developerRepository.save(this)
31+
}
3232
}
3333
?: error("No developer with type '$developerType' exists to assign for maintenance")
3434
}

src/main/resources/logback.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<configuration>
2+
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
3+
<encoder>
4+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
5+
<charset>utf8</charset>
6+
</encoder>
7+
</appender>
8+
9+
<root level="info">
10+
<appender-ref ref="CONSOLE"/>
11+
</root>
12+
13+
<logger name="codes.jakob.tstse" level="debug" additivity="false">
14+
<appender-ref ref="CONSOLE"/>
15+
</logger>
16+
</configuration>

0 commit comments

Comments
 (0)