Skip to content

Completing the Console Adapter

Do This First...

Be sure to have completed prior labs before starting.

You can also use the solution from the original repository and do a checkout of the tag lab5-start:

git checkout lab5-start

Goal

Move all console-specific behavior to the new ConsoleGameAdapter class that's been provided.

Why: to "purify" the Game class, making it I/O-Free. I/O-Free means that our Domain Objects must not reference I/O (directly or indirectly), in order to make the code easy to test and tests that run fast (on the order of a second or two).

A. Update the Codebase

You may skip this step if...

If you did a git checkout lab5-start, then the classes and changes described in the next few steps have already been done for you and you can skip to Fix Compile Errors below. You might want to read the steps anyway to find out what has changed.

Add New Classes

To jump-start this lab, I've provided two new classes: Blackjack.java and ConsoleGame.java.

The ConsoleGame has compile errors that you will fix below, so leave them alone for now.

You'll also need to add a field and some methods to the Game class.

New Field in Game

Add this field to the Game class:

private boolean playerDone;

This keeps track of the state of the player: are they still able to perform actions, or are they done (see the new methods below).

New Methods for Game

Add these methods to the Game class (I put them at the end):

public void playerHits() { // (1)!
    playerHand.drawFrom(deck);
    playerDone = playerHand.isBusted(); // (2)!
}

public void playerStands() { // (3)!
    playerDone = true; // (4)!
}

public boolean isPlayerDone() { // (5)!
    return playerDone;
}
  1. Command method representing the player's Hit action.
  2. If the player goes bust, then they are "done".
  3. Command method representing the player's Stand action.
  4. If the player stands, by definition they are "done".
  5. Query method of current state.

Update pom.xml

The Blackjack class above replaces the main() method in Game, so you'll need to update the pom.xml file so that it starts the game using the new class. Find this line:

<mainClass>com.r2ha.blackjack.domain.Game</mainClass>

and replace domain.Game with Blackjack:

<mainClass>com.r2ha.blackjack.Blackjack</mainClass>

And since we're changing the entry point into the application, let's make sure to update our GameDisplayTest. Replace

Game.main(new String[0]);

with

Blackjack.main(new String[0]);

B. Fix Compile Errors

Now you can fix the compile errors in ConsoleGame that are due to non-public visibility. Generally, making them public will be sufficient. Later, after they're moved to the right class, you'll reduce their visibility.

Run Tests & Play Game

To check that you've done everything correctly, run all the tests: they should pass. Run the game from the console, and you might run into a small problem. See if you can figure it out before looking at the solution.

Hint

When and where does the Scanner get initialized? Can we initialize it somewhere else?

C. Move Console I/O to Adapter

Less Guidance

Unlike prior labs, this section is less prescriptive, leaving it to you to figure out the right thing to do. Don't hesitate to ask for help or guidance in the Discord (or via email if you're not on Discord).

Now that everything is as it was, at least from the outside (playing the game), we can work on completing the separation of I/O-dependent code from the domain-specific logic. Your goal is to do this to the Game class by moving all code relating to Console I/O into the new ConsoleGame class. You'll know you're done when the Game class is I/O-Free.

  • For example, in determineOutcome, change the System.out.println to return a String and adjust the calling code as needed.
  • You will need to expose Game information via Query methods (this is the Encapsulation vs. I/O-Free tradeoff). Use these guidelines when creating the new methods:

    Query Method Guidelines

    When exposing data with new Query Methods, make sure they follow these guidelines:

    • Unchanging: The returned data is a point-in-time view of the data. When you do a "query", even against an object, you want to know what the information was at the point you asked for it, and don't expect the result to change even if the underlying object's data does change. Returning a copy can be useful, or returning an immutable object.

    • Integrity: Prevent consumers of the data from changing the object's internal state. It helps if the return value doesn't mislead clients into thinking it is providing access to internal state, therefore avoid returning mutable objects.

    • Safe: Calling the Query should not change the observable state of the object. This is also known as having no side-effects.

    Think and Discuss...

    When creating the query methods for the player and dealer hands (playerHand() and dealerHand()), what would returning Hand mean to the caller?

    Click for an Answer

    Returning mutable objects (like Hand) might lead the caller to believe that changes to that object will change the state of the actual hand, which breaks encapsulation. Even returning a copy is insufficient as the calling code still looks like it has a mutable object, but changing it does not affect the game's state.

    Think about all of the options for the return values from these methods (including bad ones). I've come up with four different options, can you come up with more? Share your thoughts and ideas in Discord.

Use automated refactorings as much as you can, such as Make Static (Refactor menu > Make Static...) and Move Members (F6).

Need Help?

This section is a lot of work. Struggling to get it right is how you learn, but if you find yourself struggling for too long, don't hesitate to ask for help on the Discord or via email. For those of you in the Group Guided Tour, you can also discuss and get help during the Group Coaching Hour.

D. Tidy The Code

Once you've moved all Console-related code out of the Game class, don't forget to tidy up the code in both the Game class as well as the ConsoleGame class.

Things to think about as you tidy:

  • What code is now no longer necessary in Game?

  • What static methods in ConsoleGame can be turned into instance methods?

  • What does the public API of Game need to be, i.e., what methods can be made private?

    Think About...

    What do you think of the names of the classes in the com.r2ha.blackjack.adapter.in.console package? How about the methods in those classes? Do they make it obvious what they're doing?

    Click for an Answer

    The names ConsoleGame, ConsoleCard, and ConsoleHand don't say much about the abstraction that they represent, or the roles that those objects play. What are some better names that would make their purpose more clear? How about display in the ConsoleCard class and displayFaceUpCard in the ConsoleHand class? Discuss in the Discord.

E. Play the Game!

While we have a reasonable set of characterization tests, it's still a good idea to play the game to see that everything continues to work as expected.

Remember that you'll want to do this from a full command terminal.


You're Done

That's the end of this lab. Well done!

If you want to compare your solution with mine, you can checkout from the main repository using the tag lab5-solution.