Design Patterns: Structural Patterns
- The repository for this application can be found here
Overview
- Building a Number Guess Game
- Building a Console Interface with Facade Design Pattern
- Refactoring Number Gues Game
Building a Number Guess Game
public class MainApplication {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("Enter a minimum value");
final String min = scanner.nextLine();
final Integer minInt = Integer.parseInt(min);
System.out.println("Enter a maximum value");
final String max = scanner.nextLine();
final Integer maxInt = Integer.parseInt(max);
System.out.println(String.format("Enter a guess between %s and %s", min, max));
final String guess = scanner.nextLine();
final Integer guessInt = Integer.parseInt(guess);
final Integer randomValue = ThreadLocalRandom.current().nextInt(minInt, maxInt);
if (guessInt > randomValue) {
System.out.println("Guess a lower value");
} else if (guessInt < randomValue) {
System.out.println("Guess a higher value");
} else {
System.out.println("You guessed the correct value");
break;
}
}
}
}
Building a Console Interface with Facade Design Pattern
public class InputOutputFacade {
private final InputStream in;
private final PrintStream out;
public InputOutputFacade() {
this(System.in, System.out);
}
public InputOutputFacade(InputStream in, PrintStream out) {
this.in = in;
this.out = out;
}
public void print(String valueToBePrinted, Object... optionalStringFormatters) {
out.printf(valueToBePrinted, optionalStringFormatters);
}
public void println(String valueToBePrinted, Object... optionalStringFormatters) {
this.print(valueToBePrinted + "\n", optionalStringFormatters);
}
public String getStringInput(String prompt, Object... optionalStringFormatters) {
final Scanner scanner = new Scanner(this.in);
this.println(prompt, optionalStringFormatters);
final String userInput = scanner.nextLine();
return userInput;
}
public Double getDoubleInput(String prompt, Object... optionalStringFormatters) {
final String stringInput = this.getStringInput(prompt, optionalStringFormatters);
try {
return Double.parseDouble(stringInput);
} catch(NumberFormatException nfe) { // non-numeric value
this.println("[ %s ] is an invalid input!", stringInput);
this.println("Try inputting a numeric value!");
return getDoubleInput(prompt, optionalStringFormatters); // prompt user again
}
}
public Long getLongInput(String prompt, Object... optionalStringFormatters) {
final String stringInput = this.getStringInput(prompt, optionalStringFormatters);
try {
return Long.parseLong(stringInput);
} catch(NumberFormatException nfe) { // non-numeric value
this.println("[ %s ] is an invalid input!", stringInput);
this.println("Try inputting a integer value!");
return getLongInput(prompt, optionalStringFormatters); // prompt user again
}
}
public Integer getIntegerInput(String prompt, Object... optionalStringFormatters) {
return getLongInput(prompt, optionalStringFormatters).intValue();
}
}
Refactoring Number Guess Game
public class MainApplication {
public static void main(String[] args) {
InputOutputFacade io = new InputOutputFacade();
final Integer minInt = io.getIntegerInput("Enter a minimum value");
final Integer maxInt = io.getIntegerInput("Enter a maximum value");
final Integer randomValue = ThreadLocalRandom.current().nextInt(minInt, maxInt);
while(true) {
final Integer guessInt = io.getIntegerInput("Enter a guess between %s and %s", minInt, maxInt);
if (guessInt > randomValue) {
io.println("Guess a lower value");
} else if (guessInt < randomValue) {
io.println("Guess a higher value");
} else {
io.println("You guessed the correct value");
break;
}
}
}
}
Using the Filewriter
public class MainApplication {
public static void main(String[] args) {
final InputOutputFacade io = new InputOutputFacade();
Integer numberOfGuesses = 0;
final Integer minInt = io.getIntegerInput("Enter a minimum value");
final Integer maxInt = io.getIntegerInput("Enter a maximum value");
final Integer randomValue = ThreadLocalRandom.current().nextInt(minInt, maxInt);
while (true) {
final Integer guessInt = io.getIntegerInput("Enter a guess between %s and %s", minInt, maxInt);
numberOfGuesses++;
if (guessInt > randomValue) {
io.println("Guess a lower value");
} else if (guessInt < randomValue) {
io.println("Guess a higher value");
} else {
io.println("You guessed the correct value");
break;
}
}
io.println("Number of Guesses: %s", numberOfGuesses);
final String currentProjectDirectory = System.getProperty("user.dir");
final String resourceDirectoryLocalPath = "/src/main/resources";
final String resourceDirectory = currentProjectDirectory + resourceDirectoryLocalPath;
final File file = new File(resourceDirectory);
final FileWriter fileWriter;
try {
fileWriter = new FileWriter(file, true);
fileWriter.write(numberOfGuesses.toString());
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Creating a Facade for reading and writing files
public class ReadWriteFacade {
private final File file;
public ReadWriteFacade(String fileName) {
this(new File(fileName));
}
public ReadWriteFacade(File file) {
this.file = file;
}
public void write(String content, boolean append) {
final FileWriter fileWriter;
try {
fileWriter = new FileWriter(file, append);
fileWriter.write(content);
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String read() {
final StringBuilder contents = new StringBuilder();
try {
final Scanner scanner = new Scanner(this.file);
while (scanner.hasNextLine()) {
final String currentLine = scanner.nextLine();
contents.append(currentLine);
contents.append("\n");
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
return contents.toString();
}
public List<String> toLines() {
return Arrays.asList(read().split("\n"));
}
public String read(int lineNumber) {
return toLines().get(lineNumber);
}
public void replaceLine(int lineNumber, String contentToBeWritten) {
final StringBuilder result = new StringBuilder();
final List<String> lines = this.toLines();
lines.set(lineNumber, contentToBeWritten);
lines.forEach(line -> result.append(line).append("\n"));
this.write(result.toString().replaceAll("$\n", ""), false);
}
public void replaceAllOccurrences(String stringToReplace, String replacementString) {
final String contentToWrite = read().replaceAll(stringToReplace, replacementString);
this.write(contentToWrite, false);
}
}
Creating a Resources directory Singleton
public enum DirectoryReference {
RESOURCES("/src/main/resources");
private final String directoryPath;
DirectoryReference(String localDirectoryPath) {
final String currentProjectDirectory = System.getProperty("user.dir");
this.directoryPath = currentProjectDirectory + localDirectoryPath;
}
public File getFile(String fileName) {
final File file = new File(this.directoryPath + fileName);
file.getParentFile().mkdirs();
try {
file.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}
return file;
}
public ReadWriteFacade getReadWriteFacade(String fileName) {
return new ReadWriteFacade(getFile(fileName));
}
}
Creating a RepositoryInterface
public interface RepositoryInterface<
IdType extends Serializable,
EntityType extends EntityInterface<IdType>> {
List<EntityType> findAll();
EntityType add(EntityType entityType);
EntityType delete(EntityType entityType);
EntityType updateById(IdType id, EntityType newData);
default EntityType deleteById(IdType id) {
return delete(findById(id));
}
default EntityType findById(IdType idOfEntityToFind) {
return findAll()
.stream()
.filter(Objects::nonNull)
.filter(entity -> entity.getId().equals(idOfEntityToFind))
.findFirst()
.orElse(null);
}
}
Creating an AccountRepository
public class AccountRepository implements RepositoryInterface<Long, AccountEntity> {
private final ReadWriteFacade rw;
public AccountRepository() {
final String fileName = getClass().getSimpleName();
this.rw = DirectoryReference.RESOURCES.getReadWriteFacade("/" + fileName + ".csv");
}
@Override
public List<AccountEntity> findAll() {
return rw
.toLines()
.stream()
.map(line -> {
try {
final String[] fields = line.split(",");
final Long id = Long.parseLong(fields[0]);
final String name = fields[1];
return new AccountEntity(id, name);
} catch(Throwable t) {
return null;
}
})
.collect(Collectors.toList());
}
@Override
public AccountEntity add(AccountEntity accountEntity) {
final Long id = accountEntity.getId();
if (findById(id) != null) {
final String errorMessage = "An account entity with an id of %s already exists";
throw new IllegalArgumentException(String.format(errorMessage, id));
}
rw.write(accountEntity.toString(), true);
return findById(id);
}
@Override
public AccountEntity delete(AccountEntity accountEntity) {
rw.replaceAllOccurrences(accountEntity.toString(), "");
return findById(accountEntity.getId());
}
@Override
public AccountEntity updateById(Long id, AccountEntity newData) {
final AccountEntity accountToDelete = findById(id);
rw.replaceAllOccurrences(accountToDelete.toString(), newData.toString());
return findById(id);
}
}
Testing AccountRepository
public class AccountRepositoryTest {
@Test
public void testAddMethod() {
// given
final Long id = 15L;
final String name = "Leon";
final AccountEntity account = new AccountEntity(id, name);
final AccountRepository accountRepository = new AccountRepository();
// when
final AccountEntity persistedAccount = accountRepository.add(account);
// then
Assert.assertEquals(id, persistedAccount.getId());
Assert.assertEquals(name, persistedAccount.getName());
}
}
Abstracting AccountRepository to CsvRepositoryInterface
public interface CsvRepositoryInterface<
IdType extends Serializable,
EntityType extends EntityInterface<IdType>>
extends RepositoryInterface<IdType, EntityType> {
EntityType parse(String line);
default ReadWriteFacade getReadWriteFacade() {
final String fileName = getClass().getSimpleName();
return DirectoryReference.RESOURCES.getReadWriteFacade("/" + fileName + ".csv");
}
@Override
default List<EntityType> findAll() {
return getReadWriteFacade()
.toLines()
.stream()
.map(line -> parse(line) )
.collect(Collectors.toList());
}
@Override
default EntityType add(EntityType accountEntity) {
final IdType id = accountEntity.getId();
if (findById(id) != null) {
final String errorMessage = "An account entity with an id of %s already exists";
throw new IllegalArgumentException(String.format(errorMessage, id));
}
getReadWriteFacade().write(accountEntity.toString(), true);
return findById(id);
}
@Override
default EntityType delete(EntityType accountEntity) {
getReadWriteFacade().replaceAllOccurrences(accountEntity.toString(), "");
return findById(accountEntity.getId());
}
@Override
default EntityType updateById(IdType id, EntityType newData) {
final EntityType accountToDelete = findById(id);
getReadWriteFacade().replaceAllOccurrences(accountToDelete.toString(), newData.toString());
return findById(id);
}
}
Creating AnsiColor Class
public enum AnsiCode {
BLACK(30),
RED(31),
GREEN(32),
YELLOW(33),
BLUE(34),
PURPLE(35),
CYAN(36),
WHITE(37);
private final String value;
AnsiCode(Integer ansiNumber) {
this.value = "\u001B[" + ansiNumber + "m";
}
public String getValue() {
return value;
}
}