I made a game of tic-tac-toe, but as it turned out when I started writing tests, testing it through a unit with tests was simply not possible. Help fix so that the principles of the PLO are respected.

A game:

interface FirstMove { void firstMove(); } interface Winner { Gamers getWinner(); } public interface Round extends FirstMove, Winner { } public class Game implements Round { /** * Desc for play. */ private Board desc = new Desc(); /** * Winner. */ private Gamers winner; /** * For console input. */ private In input = new Input(); /** * List contain all gamer: bot and user. */ private ArrayList<Gamers> gamers = new ArrayList<>(); /** * Util class contain algorithm determining winner. */ private Validation valid = new ValidationWinnerUtil(); /** * Getter for winner. * * @return gamer which win. */ @Override public Gamers getWinner() { return this.winner; } /** * Check correct move. * * @param player player which move. * @return true if move success. False if move fail */ private boolean move(Gamers player) { player.setPosit(); if (this.desc.getDesc()[player.getPosit().getY()][player.getPosit().getX()] == ' ') { this.desc.getDesc()[player.getPosit().getY()][player.getPosit().getX()] = player.getColor(); return true; } else { System.err.println("Так ходить нельзя!"); return false; } } /** * Determines fist move. */ @Override public void firstMove() { this.desc.initDescSize(); System.out.println("Кто ходит первым? Enter: Bot / I"); if (this.input.getStrInput().toUpperCase().equals("BOT")) { this.fstMoveBot(); } else { this.fstMoveUsr(); } Printer.printDesc(this.desc.getDesc()); this.loopMoves(); } /** * Configurable statement game if bot move first. */ private void fstMoveBot() { this.gamers.add(new Bot(Colors.X.getColor())); this.gamers.add(new User(Colors.O.getColor())); } /** * Configurable statement game if user choice move first. */ private void fstMoveUsr() { this.gamers.add(new User(Colors.X.getColor())); this.gamers.add(new Bot(Colors.O.getColor())); } /** * Loop game process. */ private void loopMoves() { Gamers winner = null; while (this.valid.gameCanGoOn(this.desc.getDesc())) { for (Gamers gamer : this.gamers) { if (this.valid.gameCanGoOn(this.desc.getDesc()) && this.move(gamer) ) { Printer.printDesc(this.desc.getDesc()); winner = gamer; } else if (this.valid.gameCanGoOn(this.desc.getDesc())) { this.mistakeMove(gamer); Printer.printDesc(this.desc.getDesc()); } } } this.initResultGame(winner); } /** * Give more chance when player which mistake - try move in busy cell. * * @param gamer player which mistake. */ private void mistakeMove(Gamers gamer) { while (!this.move(gamer)) { mistakeMove(gamer); } } /** * Init result game. * * @param winner gamer for estimated award. * @see TickTack#winners */ private void initResultGame(Gamers winner) { if (!this.valid.emptyCellExist(this.desc.getDesc()) && !this.valid.winnerDetermines(this.desc.getDesc()) ) { System.out.println("Ничья."); } else if (this.valid.winnerDetermines(this.desc.getDesc())) { this.winner = winner; System.out.println(format("Победитель: %s", winner.getColor())); } } } 

Board:

 interface StatementDesk { /** * Getter for desc. Init infoDesc. * @return desc. */ char[][] getDesc(); } interface DescSize { /** * Init desc size in starting game. */ void initDescSize(); } public interface Board extends StatementDesk, DescSize, StubInputInterface { } public class Desc implements Board { /** * Information about statement desc for bot. */ private static char[][] infoDesc; /** * Input for get console in. */ private In input = new Input(); /** * Desc for game. */ private char[][] desc = new char[3][3]; /** * Pointer on desc. It's information for bot about statement desc. * * @return current statement desc. */ public static char[][] getInfoDesc() { return infoDesc; } /** * Use for test class StubInput. * * @param input emulation console input stream. */ @Override public void setInput(In input) { this.input = input; } /** * Getter for desc. Init infoDesc. * * @return desc. */ @Override public char[][] getDesc() { infoDesc = this.desc; return this.desc; } /** * For choice desc size. */ @Override public void initDescSize() { System.out.println("Хотите использовать стандартный размер поля: y/n"); String answer = this.input.getStrInput(); if (answer.equals("y")) { System.out.println("Установлен стандартный размер поля: 3/3"); } else if (answer.equals("n")) { this.initNonstandardDesc(); } this.initContentDesc(); } /** * Install desc nonstandard size. */ private void initNonstandardDesc() { System.out.println("Введите размер сторон:"); int i = this.input.getNumInput(); this.desc = new char[i][i]; System.out.println(format("Установлен размер поля: %s/%s", i, i)); } /** * Fill desc empty call. Empty is ' '. */ private void initContentDesc() { for (int i = 0; i != this.desc.length; i++) { for (int j = 0; j != this.desc.length; j++) { this.desc[j][i] = ' '; } } } } 

Input from console:

 public class Input implements In { /** * Input stream to console. */ private Scanner scanner = new Scanner(System.in); /** * Console input for string. * * @return input string. */ @Override public String getStrInput() { return this.scanner.next(); } /** * Console input for int. * * @return input int. */ @Override public int getNumInput() { return this.scanner.nextInt(); } } 

User:

 public class User implements Gamers, StubInputInterface { /** * Side on current game. May be 'X' or 'O'. */ private final char color; /** * Position for move. */ private Position posit = new Posit(); /** * Console input for get data from user. */ private In input = new Input(); /** * Default constructor. * * @param color 'X' or 'O'. */ public User(char color) { this.color = color; } /** * Getter for position. * * @return position. */ @Override public Position getPosit() { return this.posit; } /** * Getter for color. * * @return color. */ @Override public char getColor() { return this.color; } /** * For get data about move position from user by console input. */ @Override public void setPosit() { System.out.println("По вертикали: "); this.posit.setX(this.input.getNumInput()); System.out.println("По горизонтали: "); this.posit.setY(this.input.getNumInput()); } /** * This setter for tests. He need for use StubInput class. * * @param input emulation console input. */ @Override public void setInput(In input) { this.input = input; } } 

Computer user:

 public class Bot implements Gamers { /** * Side for battle. */ private final char color; /** * Current position. */ private Position posit; /** * Default constructor. * * @param color side battle. Init only once in begin game. */ public Bot(char color) { this.color = color; } /** * Getter gor color. * * @return color. May be 'X' or 'O'. */ @Override public char getColor() { return this.color; } /** * Getter for current position. * * @return current position. */ @Override public Position getPosit() { return this.posit; } /** * Setter for position. */ @Override public void setPosit() { this.posit = this.generatePos(); } /** * Generator new positions. * * @return position. */ private Position generatePos() { char[][] desc = Desc.getInfoDesc(); for (int i = 0; i < desc.length; i++) { for (int j = 0; j < desc.length; j++) { if (desc[j][i] == ' ') { return new Posit(i, j); } } } System.err.println("Что-то пошло не так"); return new Posit(); } } 

The class that creates a series of games and that makes the game up to five victories:

 class TickTack { /** * Contain gamers every time his win. */ private ArrayList<Gamers> winners = new ArrayList<>(); /** * Contain all games. */ private ArrayList<Round> games = new ArrayList<>(); /** * Default constructor. */ TickTack() { this.initGames(); } /** * Init all games. */ private void initGames() { for (int i = 0; i < 10; i++) { this.games.add(new Game()); } } /** * Loop games to 5 points. * After 5 wins one of players - break. */ void start() { for (Round game : this.games) { if (this.resultAllGames(winners).equals("")) { game.firstMove(); addWinner(game); } else { System.out.println(String.format( "Победитель: %s", this.resultAllGames(winners))); break; } } } /** * Add winner in list goals. * * @param game held game. */ private void addWinner(Round game) { if (game.getWinner() != null) this.winners.add(game.getWinner()); } /** * Check wins to five points. * * @param users list involved gamers. * @return "" if 5 points not success. if 5 points success name winner. */ private String resultAllGames(ArrayList<Gamers> users) { int u = 0, b = 0; for (Gamers obj : users) { if (obj instanceof User) { u++; if (u == 5) return "user"; } else { b++; if (b == 5) return "bot"; } } return ""; } } 

Here it is necessary to correct it in the style of the OOD. Well corrected myself need to tell how and what. Here it is.

  • one
    select one class. describe its interface in words. give full code. - Mikhail Vaysman
  • Here is the Game class, he is responsible for all movements on the board, he turns the moves from one user in the cycle, then from another until he determines the winner. Its interfaces are added to the beginning. - Pavel
  • one
    select the class with the fewest dependencies. the easiest to start with. - Mikhail Vaysman
  • Then Desc. Added interface code. It must store and create the array-board itself, during creation it must be possible to select the size of the board. - Pavel
  • I will give my version, after some time - Mikhail Vaysman

1 answer 1

Here is the primary option. It is greatly simplified and I cut off the corners a little. But in general, the idea should be clear.

Class Board

  • responsible for keeping the playing field
  • allows you to change the state of the playing field
  • converts the playing field to a string

I did it according to the principle: first test - then code.

 package tictactoe; import org.junit.Test; import java.util.Arrays; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; public class BoardTest { @Test public void constructor_createBoardSpecifiedSize() { Board subject = new Board(1); assertEquals(1, subject.getSize()); } @Test public void getCell_onNewBoardReturnEmptyCell() throws Exception { Board subject = new Board(1); assertEquals(Cell.EMPTY, subject.getCell(0, 0)); } @Test public void putMark_onNewBoardPutThisMarkOnBoard() throws Exception { Board subject = new Board(1); subject.putMark(Cell.X, 0, 0); assertEquals(Cell.X, subject.getCell(0, 0)); } @Test(expected = IllegalMove.class) public void putMark_inOccupiedCellThrowsException() throws Exception { Board subject = new Board(1); subject.putMark(Cell.X, 0, 0); subject.putMark(Cell.O, 0, 0); } @Test(expected = IllegalMove.class) public void putMark_EmptyMarkThrowsException() throws Exception { Board subject = new Board(1); subject.putMark(Cell.EMPTY, 0, 0); } @Test public void toString_onNewReturnsEmptyBoard() throws Exception { Board subject = new Board(3); assertEquals("+-+-+-+\n" + "| | | |\n" + "+-+-+-+\n" + "| | | |\n" + "+-+-+-+\n" + "| | | |\n" + "+-+-+-+\n", subject.toString()); } @Test public void toString_onBoardWithTwoMarksReturnsCorrectBoard() throws Exception { Board subject = new Board(3); subject.putMark(Cell.X, 0, 0); subject.putMark(Cell.O, 2, 2); assertEquals("+-+-+-+\n" + "|x| | |\n" + "+-+-+-+\n" + "| | | |\n" + "+-+-+-+\n" + "| | |o|\n" + "+-+-+-+\n", subject.toString()); } } class Board { private Cell[][] board; private int size; public Board(int size) { this.size = size; board = new Cell[size][size]; for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { board[i][j] = Cell.EMPTY; } } } public Cell getCell(int row, int col) { return board[row][col]; } public int getSize() { return size; } public void putMark(Cell mark, int row, int col) throws IllegalMove { if(mark == Cell.EMPTY || board[row][col] != Cell.EMPTY) { throw new IllegalMove(); } board[row][col] = mark; } @Override public String toString() { StringBuffer result = new StringBuffer(); for (int i = 0; i < size; i++) { result.append(row(i)); } result.append(horizontalLine()); return result.toString(); } private StringBuffer row(int r) { StringBuffer row = new StringBuffer(); row.append(horizontalLine()); row.append("|"); row.append(Arrays.asList(board[r]).stream().map(Cell::toString).collect(Collectors.joining("|"))); row.append("|\n"); return row; } private StringBuffer horizontalLine() { StringBuffer line = new StringBuffer(); for (int i = 0; i < size; i++) { line.append("+-"); } line.append("+\n"); return line; } } enum Cell { EMPTY(" "), X("x"), O("o"); private String symbol; Cell(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } } class IllegalMove extends Exception {} 
  • one
    And what solutions to this see? - Mikhail Vaysman
  • one
    If there are more than 4 arguments, then it makes sense to look at the design again. If you give a definition for the word easier, it will be clearer. In general, this method is bad. It is better to completely recycle, and maybe even remove. - Mikhail Vaysman
  • one
    Just ignore this method. If it is difficult, then I can remove it. - Mikhail Vaysman
  • one
    you should not have a lot of user input dependent classes. Read about SOLID. - Mikhail Vaysman
  • one
    so I’m just going through them))) it turns out I’m breaking SRP by poking input everywhere and everywhere ... - Pavel