Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real Coding: Requirements, Bugs, and Design Flaws

DZone's Guide to

Real Coding: Requirements, Bugs, and Design Flaws

Requirements, bugs, tests, design flaws—so much can happen in 25 minutes when you do "real coding". Check out the newest installment of the series.

· Java Zone
Free Resource

Build vs Buy a Data Quality Solution: Which is Best for You? Gain insights on a hybrid approach. Download white paper now!

Welcome to the second installment of my "Real Coding" series, in which I'm aiming to develop an "enterprise" ranking application in about two weeks, working only for one Pomodoro per day. If you want to read more about the requirements or my motivation for writing this series, make sure to check out the first part of the series.

Where We Left Off

I will do my best to keep each of the posts as separate from the others as possible so that you don't have to read everything way back to get a grasp of what's happening. Having said that, let's take a look at the code that was left after the previous Pomodoro:

package com.tidyjava.rankingapp

import java.util.*

class Ranking(val defaultRating: Rating,
              val ratings: MutableMap<PlayerID, Rating>,
              val matches: MutableList<Match>) {

    fun join(playerID: PlayerID) {
        ratings.put(playerID, defaultRating)
    }
}

class Match(val id: MatchID) {
}

data class MatchID(val value: UUID = UUID.randomUUID())

data class PlayerID(val value: String)

data class Rating(val value: Int)


To give you some context, this was all written in the last 5 minutes of the previous Pomodoro, so it was not really thought through. Rather, I wanted to get to a starting point for further work.

The design idea that I decided to go with is that all rankings in the application will be aggregates that keep track of the match history and players' ratings.

More Analysis!

Instead of getting straight back to code, I started this Pomodoro by writing down all the application's functionality necessary for making any productive use of the application. The purpose of this little "exercise" is to increase both my and your awareness of what's about to happen next in the project and to get a better perspective on the emerging domain model. In the end, I came up with the following few points:

  • Log in: Obviously, you can't do much without that.

  • Join a ranking: Since last time, I decided that you need to "join" a ranking to be able to play matches. It seemed like a better choice if the application was to be used outside of a single company.

  • Add a match result: Rankings without games make no sense.

  • Confirm/change the match result: As said before, the players should confirm their wins/losses before any ranking adjustments are made.

  • View the current standings: I guess that's the whole point of the application.

Whoops, I Coded "For the Future"!

Having already done some extra analysis, I had around 20 minutes left to finally do some "real coding". I started off by removing the MatchID and PlayerID classes, as nicely suggested in a comment by Robert Brautigam. (Thanks, Robert!) Here's an important rule of programming that even the most experienced programmers seem to break fairly often: Do not introduce things that you do not need yet, especially if they are addressing purely technical concerns, like identity in a database.

Too Trivial or Not Too Trivial?

The next thing I set my eyes on was the join method of the Ranking class. I considered testing that for a short while, but eventually, I decided not to. This method seemed to be doing the job and the implementation is trivial, so why bother? Well, just as I was writing that sentence a few seconds ago, I realized that it doesn't do the job well because it doesn't handle the case when someone is already ranked in a given ranking. This method would reset the player's score! I guess I'll start the next Pomodoro by fixing that bug.

Adding a Match. Trivial, Again?

Being entirely convinced that the joining functionality is ready, I set my eyes on adding a match to the ranking. I started off with the simplest possible implementation:

fun addMatch(match: Match) {
    matches.add(match)
}


Ha, trivial again! Well, that changed a bit a few minutes later when I realized a possible case of adding a match between players that are not in a given ranking. This made the implementation evolve to:

fun addMatch(match: Match) {
    requireRanked(match.player1)
    requireRanked(match.player2)
    matches.add(match)
}

private fun requireRanked(player: Player) {
    if (!ratings.containsKey(player))
        throw PlayerNotRankedException(player)
}


Since we've got an if statement, with a negation and exception in here, I decided to write a little test for this functionality, especially given that I messed this up a while before. (To be entirely precise, the test was the last thing that I wrote in this Pomodoro, but this order makes more sense from the article's perspective).

package com.tidyjava.rankingapp

import com.tidyjava.rankingapp.MatchResult.PLAYER1_WON
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.time.LocalDateTime

class RankingTest {
    private val matchTime = LocalDateTime.of(2017, 8, 18, 21, 0)
    private val grzegorz = Player()
    private val james = Player()
    private val smackdown = Match(matchTime, grzegorz, james, PLAYER1_WON)

    private val ranking = Ranking("DZone Smackdown?!", Rating(1200))

    @Test
    fun cannotPlayWithoutJoining() {
        assertThrows(PlayerNotRankedException::class.java) {
            ranking.addMatch(smackdown)
        }
    }

    @Test
    fun canPlayAfterJoining() {
        ranking.join(grzegorz)
        ranking.join(james)

        ranking.addMatch(smackdown)

        assertEquals(listOf(smackdown), ranking.matches)
    }
}


If you're not familiar with the assertThrows syntax, I highly recommend you getting to know JUnit 5, as it's about to become the new standard in Java testing soonish!

Confirming a Result

The last thing that I got a chance to put my hands on during this Pomodoro was the possibility of confirming a previously submitted match result. Obviously, I started off by specifying what the possible results are:

class Match(val player1: Player,
            val player2: Player,
            var result: MatchResult) {
}

enum class MatchResult {
    PLAYER1_WON,
    PLAYER2_WON,
    DRAW
}


I'm not entirely sure whether this enum is the best way to represent the final score, but it was the first thing that came to my mind, so I went for it. Right now, I'm thinking about changing this to some kind of a numerical score. We'll see.

Also, I decided to ignore the requirement of supporting both solo and team games for now. I'll try to get the application working for solo players and only then focus on supporting teams.

The next step to be taken was to actually mark the match result as confirmed. For this reason, I introduced yet another enum:

enum class MatchStatus {
    WAITING_FOR_PLAYER1, WAITING_FOR_PLAYER2, CONFIRMED
}


To be honest, I hate using enums like this for some reason. It just doesn't feel right. At the same time, there's only so much that can be done in 25 minutes, and this seems to be just good enough for the purpose. Anyway, suggestions for improvement will be highly appreciated!

The last thing that I managed to do in this matter was actually setting the confirmed status of the match. This required me to invent some way of uniquely identifying a match. A natural ID for a match in the application would be the players that participate in the match and the date when the match has taken place. And so I decided to add the dateTimefield to the Match class:

class Match(val dateTime: LocalDateTime,
            val player1: Player,
            val player2: Player,
            var result: MatchResult,
            var status: MatchStatus = MatchStatus.WAITING_FOR_PLAYER2) {
}


At this point, I could have written a confirmation method that recognizes a match by the three first fields, but I decided not to. My intuition suggested that for some reason, using an artificial identifier will yield a less cumbersome API than passing these three objects everywhere I want to refer to a single match. And so the MatchIDclass was reintroduced:

class Match(val dateTime: LocalDateTime,
            val player1: Player,
            val player2: Player,
            var result: MatchResult,
            var status: MatchStatus = MatchStatus.WAITING_FOR_PLAYER2,
            val id: MatchId = MatchId()) {
}

data class MatchId(val value: UUID = UUID.randomUUID())


Feel free to disagree with me about that, but it feels like a good decision for now.

With this in place, I was finally ready to start implementing the confirmation method. As you can see below, it's not really complete yet:

fun confirmResult(id: MatchId) {
    val match = matches.find { it.id == id } ?: throw MatchNotFoundException(id)
    match.status = MatchStatus.CONFIRMED
}


Final Words

Basically, that's the point when I ran out of time for this session, so that's the code — at least until tomorrow. If you see anymore lurking bugs, missing requirements, or design flaws, please feel free to mention it in the comments. If I don't change my mind and all goes well, the next installment should be pretty exciting, as we're about to recalculate the player ratings after confirming a match result. Stay tuned!

P.S. I uploaded the project to GitHub if anybody's interested in seeing the two source files in their entirety!

Build vs Buy a Data Quality Solution: Which is Best for You? Maintaining high quality data is essential for operational efficiency, meaningful analytics and good long-term customer relationships. But, when dealing with multiple sources of data, data quality becomes complex, so you need to know when you should build a custom data quality tools effort over canned solutions. Download our whitepaper for more insights into a hybrid approach.

Topics:
kotlin ,pomodoro technique ,design flaws ,real coding ,java ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}