Dependency Inversion Principle
Coupling is a big problem when your code starts growing. In our first monolithic version we are calling input/output functions several times. What happens if you want to replace the CLI console with a GUI? You will need to change code all over the place. Of course, you can create a class to narrow the changes to only one class, in some way you will be narrowing your dependency on those external libraries.
But if you want to change the GUI, for example AWT, SWT, JavaFX, … You will need to change the calls to the class everywhere…
So the next step is just creating an interface for your external needs and let those classes to implement it with calls to the specific libraries. In our very basic example, we only need two functions:
- readChar (input)
- printLine (output)
We don't care how they work, we don't care about the details. Our game will depend on interfaces that we own, anybody could implement them and change the look like of the game.
We have not yet finished. Even though we have interfaces, we will need to create objects to do the work, so we still need a reference to the specific classes that implement them. At least, at the moment when we create them.
IOutput out = new GatewayConsoleOutput();
IInput in = new GatewayConsoleInput();The last step is to move all those initializations to a factory interface/class, so we will only need an external reference.
IIOFactory factory = new IOFactory();
IInput in = factory.makeConsoleInput();
IOutput out = factory.makeConsoleOutput();So, just to remove the references to input/output in the code, we will need 3 interfaces and 3 classes. Of course, it makes no sense for a simple/small game. It makes sense when you start growing the game.
package manager;
import gateways.IOFactory;
import hangman.Play;
public class Main {
public static void main(String[] args) {
IIOFactory factory = new IOFactory();
IInput in = factory.makeConsoleInput();
IOutput out = factory.makeConsoleOutput();
out.printLine("HANGMAN. New Play.");
int attempts = 7;
Play hangman = new Play(attempts);
String word = hangman.GetCurrentRevealedWord('_');
out.printLine(">>> " + word + " <<<");
// 2. Game loop
boolean success = false;
while (attempts > 0 && !success) {
// 2.1 User input
char mchar = in.readChar();
// 2.2 Logic check
boolean result = hangman.Try(mchar);
// 2.3 Win/Lose check
success = hangman.isWordCompleatedRevealed();
// 2.4 Output
if ( true == result ) {
word = hangman.GetCurrentRevealedWord('_');
out.printLine(">>> " + word + " <<<");
if ( success ) {
out.printLine("YOU WIN");
}
} else {
attempts--;
if ( attempts > 0 ) {
out.printLine("try again. " + attempts + " left.");
} else {
out.printLine("You lose. It was " + hangman.GetSecretWord());
}
}
}
// 3. Game close
}
}
package manager;
public interface IIOFactory {
IInput makeConsoleInput();
IOutput makeConsoleOutput();
}package manager;
public interface IInput {
char readChar();
}package manager;
public interface IOutput {
void printLine(String string);
}One cool thing about this approach is that you can test or mock all your dependencies. It is highly recommended for Test Driven Design.
For example, we can create a mock class that implements the IInput interface, that way we can run automatic games, or use an exact sequence of input characters for a test or use AI to play our game.
package gateways;
import java.util.Random;
import java.util.Scanner;
import manager.IInput;
public class GatewayConsoleInput implements IInput {
private Scanner scin;
public GatewayConsoleInput() {
this.scin = new Scanner(System.in);
}
// @Override
// public char readChar() {
// return this.scin.nextLine().charAt(0);
// }
@Override
public char readChar() {
Random rnd = new Random();
char c = (char) ('a' + rnd.nextInt(26));
System.out.println(c);
return c;
}
}A game using the random version of IInput.readChar().
HANGMAN. New Play.
>>> _ _ _ _ _ <<<
c
try again. 6 left.
d
try again. 5 left.
n
>>> _ _ n _ _ <<<
x
try again. 4 left.
g
>>> _ _ n g _ <<<
a
>>> _ a n g _ <<<
h
try again. 3 left.
r
try again. 2 left.
m
>>> m a n g _ <<<
x
try again. 1 left.
x
You lose. It was mango
Discussion