Live Coding Learnings - June 21, 2019
Ted M. Young
Posted on June 23, 2019
Quizdown: Episode 20
The 20th episode of working on Quizdown, a Java+Spring-based app that uses a Markdown-like syntax to create coding quizzes.
Stories Implemented
After a non-trivial refactoring (see No Naked Data Structures below),
I now have the ability to move back and forth between questions. The HTML generation now generates checked
if that choice was previously selected. Next step is to load the user's responses from the database and send that to the UI controller.
HTML: Use disabled Attribute for Button or Link
When I was looking for the proper CSS class to use to display a button as disabled, I found that instead of using, say, an is-disabled
CSS class, HTML supports disabled
as an attribute on both button
and link (<a>
). For example, to show the link as disabled instead of:
<a class="is-disabled" href="/question/0">Previous Question</a>
you would use the disabled
attribute like this:
<a href="/question/0" disabled>Previous Question</a>
No Naked Data Structures
One of my Java design heuristics is No Naked Data Structures, which means wrapping raw data structures (List
, Map
, etc.) in a type so you can pass it around and interact with it in a way that makes sense from a domain point of view.
This is similar to the No Stringly-Typed Code heuristic: don't use
String
everywhere, instead use class and interface types. For details see here and here.
When adding the feature to allow users to go back and forth through the questions, I needed to have the choices that the user made be available. Previously I was storing those choices as a Set<String>
, as it was straightforward and it wasn't being used in many places. However, once I needed it in more places (pushing it back up to the front-end from the database), handing around a "naked" Set
made the code a bit harder to read and work with. For example, with the Set
, figuring out if it's correct depended on the type of question (FIB
is a fill-in-the-blank and MC
is multiple-choice):
public boolean isCorrectFor(@NonNull Set<String> response) {
return switch (questionType) {
case FIB -> correctChoices.containsAll(response);
case MC -> correctChoices.equals(response);
};
}
This is pretty typical code, but unless you're familiar with the specifics of Set
, you may have to do a bit of JavaDoc reading to ensure you understand it correctly. By replacing it with a Response
type that encapsulates the Set
, the code above becomes:
public boolean isCorrectFor(Response response) {
return switch (questionType) {
case FIB -> response.matchesAny(correctResponse);
case MC -> response.allMatch(correctResponse);
};
}
Now, this may not be such an amazing improvement (I'm still toying with the naming), but it leads to the next step (which I haven't done yet) of pushing the behavior that's specific to the question type into a subclass of Response, e.g., a FibResponse
and McResponse
, which would look like:
public boolean isCorrectFor(Response response) {
return response.correctlyMatches(correctResponse);
}
Completely delegating how responses are compared to the class that knows best how to do that comparison.
In my training and coaching of Java developers, the use of Stringly types and Naked Data Structures is the cause for much pain when things change, so always try and encapsulate Strings into Value Objects and wrap up your data structures into its own class.
Exposing asSet()
on Response: breaks Encapsulation?
By encapsulating the Set<String>
inside of Response
means that when I need to display the response choices or persist them to a database (i.e., hand the response across the domain boundary) I need to get those choices as a set, so the Response
class has a method
public Set<String> asSet() {
return Set.copyOf(response); // return a copy of our internal field
}
That returns the choices as a Set<String>
. I use this in the GradedAnswerView
class to convert it to a comma-separated String
:
private static String responseAsString(Answer answer) {
return answer.response().asSet()
.stream()
.sorted()
.collect(Collectors.joining(", "));
}
You might think: oh no, this is breaking encapsulation by exposing the set, but I'm not really doing that, because I'm returning a copy of my internal storage of the response choices. I never expose the field itself, so the only way to modify the internal Response
data is only through its methods. Modifying the copy would have no effect.
Empty varargs
I don't think I had realized this before, but calling a method defined with varargs (e.g., createFrom(String... strings)
) means that you can call that method with no arguments: createFrom()
, and inside this method, the strings
argument would be an empty array.
This means I didn't have to special-case turning varargs into an empty Set, because calling Set.of()
with no arguments will turn into an empty Set (technically an ImmutableCollections.emptySet()
as of Java 9), which is what I'd want.
Collaboration Tests
Most of my tests are unit tests, in that they are testing code in a class by working with that class as closely as possible. For example, the Choice
class can be directly tested without any other objects. However, the QuestionTransformer
can't be tested on its own as its job is mostly to collaborate with more specific transformers to do its job, i.e., its job is to integrate or collaborate with the other objects. This means that I don't need to comprehensively test all of the behavior of all of the classes that are involved, just a test or two that would ensure that QuestionTransformer
is correctly collaborating with (in this case, propagating the response to) other objects.
You can see a clip from my stream where I do this: https://www.twitch.tv/videos/443315061.
Be sure and tune in to my next live coding stream: https://twitch.tv/jitterted.
Want to support me? Become a Patron at https://patreon.com/jitterted
Posted on June 23, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.