Mastering the art of designing scalable and efficient systems is essential in object-oriented design (OOD) interviews. One common scenario presented to software engineers during these interviews is designing a game (e.g., Blackjack, Connect Four, Chess, Jigsaw puzzle, etc.). This seemingly straightforward problem encompasses various design principles and considerations, making it an excellent litmus test for a candidate’s OOD skills.
This blog delves into the intricacies of designing a Connect Four game using object-oriented principles, patterns, and best practices. We’ll leverage a comprehensive implementation that covers various game components, including the game board, player management, game logic, user interface, and more. Additionally, we’ll explore an OOD interview guide, providing insights and guidelines for effectively navigating low-level design interviews.
Connect Four is a two-player strategy game where the objective is to be the first to connect four of one’s discs of the same color vertically, horizontally, or diagonally on a grid. Players take turns dropping colored discs into a vertical grid. The game continues until a player achieves the required four-in-a-row alignment, resulting in a win, or until the grid is filled, resulting in a draw. The game involves strategic planning and pattern recognition to outmaneuver the opponent and create winning formations.
Let’s define the requirements for the Connect Four game problem:
The game must support two players, each identified by a unique marker (X and O), each with a fixed number of disks (21).
Create a 6×7 grid game board where players can place markers. The board starts empty.
Players take turns dropping their markers into a chosen column, which should be the lowest available position in that column.
Players win if they connect four of their markers horizontally, vertically, or diagonally.
Players alternate turns throughout the game; the first turn should be through a coin toss.
The game ends when a player wins or the board is filled (resulting in a draw).
These core functionalities are needed to run the Connect Four game smoothly in an object-oriented design paradigm. As we proceed, these requirements will help shape the classes and interfaces we’ll construct to simulate the game.
In the class diagram for the Connect Four game, we’ll identify and design classes, abstract classes, and interfaces based on the game’s requirements.
As we take a bottom-up approach to designing the Connect Four game, we’ll begin by identifying and designing the classes for smaller components like the game board, players, and win condition strategies. Subsequently, we’ll integrate these smaller components into the class representing the Connect Four game system.
Let’s look at the different components of a Connect Four game system.
The Player
class represents a player in the game. It is characterized by two instance variables: name
(the player’s name) and marker
(the marker used by the player). It has getter methods for these instance variables.
The WinConditionStrategy
interface represents a win condition check strategy. It declares a method, checkForWin
, that takes a grid
and a marker
and checks if there’s a win condition for the marker in the grid.
Three classes (VerticalWinCondition
, HorizontalWinCondition
, and DiagonalWinCondition
) implement this interface, each checking for a win in a different direction.
The GameBoard
class represents the game board in the Connect Four game. It has a 2D array grid
representing the game grid and a list of WinConditionStrategy
objects. It has several methods for handling game board-level operations, such as initializing the grid, checking for a win condition, checking if the board is full, making a move, and printing the board.
The GameObserver
interface defines the update
method for observer classes. The PlayerObserver
class, which implements the GameObserver
interface, has a Player
instance variable and provides a player-specific update functionality.
The Game
class is one of the main classes that handle the game’s overall logic. It has instance variables for player1
, player2
, currentPlayer
, gameBoard
, and a list of GameObserver
. It includes methods to start the game, switch between players, notify updates to observers, and handle user inputs.
The Toss
class is used to decide which player starts the game.
The GameFactory
class supplies instance creation logic for Player
, GameBoard
, and Game
classes.
The GameState
class represents the game’s current state, with variables representing the game board grid, the current player’s marker, and any relevant message.
Now, let’s examine the relationships between the classes defined in the Connect Four game system.
The class diagram exhibits the following association relationships:
The GameFactory
has a one-way association with Player
, Game
, and GameBoard
.
The GameObserver
has a one-way association with GameState
.
The Toss
has a one-way association with Player
.
The GameBoard
has a one-way association with WinConditionStrategy
.
The class diagram has the following aggregation relationships:
The Game
has an aggregation relationship with Player
, GameBoard
, and GameBoard
.
The following classes show an inheritance relationship:
The VerticalWinCondition
, HorizontalWinCondition
, and DiagonalWinCondition
classes inherit from the WinConditionStrategy
class.
PlayerObserver
can be considered as a specific type of GameObserver
. Therefore, this can also be seen as an inheritance relationship, with PlayerObserver
as the subclass and GameObserver
as the superclass.
Note: We have already discussed the inheritance relationship between classes in the class diagram section above.
Here is the complete class diagram for our Connect Four game:
In the implementation of the Connect Four game, several design patterns are utilized to enhance the structure, maintainability, and flexibility of the system:
Design Patterns | Description |
Factory pattern | The |
Observer pattern | We’ve used the observer design pattern in the Connect Four game design. With this pattern, every time there’s an update in the game state, all the observers (in our case, players) get notified. |
Strategy pattern | The |
The SOLID principles aim to make software designs more understandable, flexible, and maintainable. They consist of:
Design Principles | Description |
Single responsibility principle (SRP) | Each class has a single well-defined responsibility. For instance, the |
Open-closed principle (OCP) | The classes are open for extension but closed for modification. The |
Liskov substitution principle (LSP) | This is prominent in the use of the |
Interface segregation principle (ISP) | We’ve used specific interfaces (e.g., |
Dependency inversion principle (DIP) | The |
We’ve discussed the design aspects of the Connect Four game and identified the necessary classes and their relationships. Now, let’s look at the practical implementation of the Connect Four game using Java. This implementation will bring our design to life and allow us to simulate and play the game.
We have chosen Java for the implementation of the Connect Four game.
The Player
class represents a player in the game, storing their name and marker.
class Player {private final String name;private final String marker;public Player(String name, String marker) {this.name = name;this.marker = marker;}public String getName() {return name;}public String getMarker() {return marker;}}
The WinConditionStrategy
interface defines a strategy for checking win conditions on the game board. The following are the three child classes of the WinConditionStrategy
interface:
The HorizontalWinCondition
class implements the WinConditionStrategy
interface to check for horizontal win conditions on the game board.
The VerticalWinCondition
class implements the WinConditionStrategy
interface to check for vertical win conditions on the game board.
The DiagonalWinCondition
class implements the WinConditionStrategy
interface to check for diagonal win conditions on the game board.
interface WinConditionStrategy {boolean checkForWin(String[][] grid, String marker);}class VerticalWinCondition implements WinConditionStrategy {@Overridepublic boolean checkForWin(String[][] grid, String marker) {for (int col = 0; col < grid[0].length; col++) {for (int row = 0; row < grid.length - 3; row++) {if (marker.equals(grid[row][col]) &&marker.equals(grid[row + 1][col]) &&marker.equals(grid[row + 2][col]) &&marker.equals(grid[row + 3][col])) {return true;}}}return false;}}class HorizontalWinCondition implements WinConditionStrategy {@Overridepublic boolean checkForWin(String[][] grid, String marker) {for (int row = 0; row < grid.length; row++) {for (int col = 0; col < grid[row].length - 3; col++) {if (marker.equals(grid[row][col]) &&marker.equals(grid[row][col + 1]) &&marker.equals(grid[row][col + 2]) &&marker.equals(grid[row][col + 3])) {return true;}}}return false;}}class DiagonalWinCondition implements WinConditionStrategy {@Overridepublic boolean checkForWin(String[][] grid, String marker) {// Check for descending diagonal winfor (int row = 0; row < grid.length - 3; row++) {for (int col = 0; col < grid[row].length - 3; col++) {if (marker.equals(grid[row][col]) &&marker.equals(grid[row + 1][col + 1]) &&marker.equals(grid[row + 2][col + 2]) &&marker.equals(grid[row + 3][col + 3])) {return true;}}}// Check for ascending diagonal winfor (int row = 3; row < grid.length; row++) {for (int col = 0; col < grid[row].length - 3; col++) {if (marker.equals(grid[row][col]) &&marker.equals(grid[row - 1][col + 1]) &&marker.equals(grid[row - 2][col + 2]) &&marker.equals(grid[row - 3][col + 3])) {return true;}}}return false;}}
The GameBoard
class represents the game board, including its dimensions, grid, and functionalities like making moves, checking for wins, and printing the board.
class GameBoard {private final int rows = 6;private final int columns = 7;private final String[][] grid = new String[rows][columns];private final List<WinConditionStrategy> winConditions;private boolean gameOver;private String winMessage;public GameBoard(List<WinConditionStrategy> winConditions) {this.winConditions = winConditions;initializeGrid();}private void initializeGrid() {for (int i = 0; i < rows; i++) {Arrays.fill(grid[i], "-");}}public boolean checkForWin(String marker) {return winConditions.stream().anyMatch(strategy -> strategy.checkForWin(grid, marker));}public boolean isBoardFull() {return !Arrays.stream(grid).flatMap(Arrays::stream).anyMatch(cell -> cell.equals("-"));}public boolean makeMove(int column, String marker) {if (isValidMove(column)) {int row = findEmptyRow(column);grid[row][column] = marker;return true;}return false;}private boolean isValidMove(int column) {return column >= 0 && column < columns && grid[0][column].equals("-");}private int findEmptyRow(int column) {for (int i = rows - 1; i >= 0; i--) {if (grid[i][column].equals("-")) {return i;}}throw new IllegalArgumentException("Column " + (column + 1) + " is full.");}public void printBoard() {for (String[] row : grid) {System.out.println(Arrays.toString(row));}}public int getRows() {return rows;}public int getColumns() {return columns;}public String[][] getGrid() {return grid;}public boolean isGameOver() {return gameOver;}public void setGameOver(boolean gameOver) {this.gameOver = gameOver;}public String getWinMessage() {return winMessage;}public void setWinMessage(String winMessage) {this.winMessage = winMessage;}}
The GameObserver
interface defines a contract for classes that want to observe the state of the game. Classes implementing this interface must provide an update
method that takes a GameState
object as an argument. The PlayerObserver
class implements the GameObserver
interface and observes the game state for a specific player.
interface GameObserver {void update(GameState state, int nextMove);}class PlayerObserver implements GameObserver {private final Player player;public PlayerObserver(Player player) {this.player = player;}@Overridepublic void update(GameState state, int nextMove) {// Implement player-specific updates based on the game state and next move// For example, display a message to the playerSystem.out.println("Game state updated for " + player.getName() + ". Next move: " + nextMove);}}
The Game
class represents the game logic, including player turns, moves, and win/draw conditions.
class Game {private final Player player1;private final Player player2;private final GameBoard gameBoard;private Player currentPlayer;private final Scanner scanner;private final List<GameObserver> observers;public Game(Player player1, Player player2, GameBoard gameBoard) {this.player1 = player1;this.player2 = player2;this.gameBoard = gameBoard;this.currentPlayer = Toss.performToss(player1, player2);this.scanner = new Scanner(System.in);this.observers = new ArrayList<>();this.observers.add(new PlayerObserver(player1));this.observers.add(new PlayerObserver(player2));}public void addObserver(GameObserver observer) {this.observers.add(observer);}public void play() {System.out.println("Game Started. " + currentPlayer.getName() + " begins.");boolean gameRunning = true;while (gameRunning) {gameBoard.printBoard();System.out.println(currentPlayer.getName() + "'s turn. Enter column number (1-" + gameBoard.getColumns() + "): ");int column = scanner.nextInt() - 1;if (gameBoard.makeMove(column, currentPlayer.getMarker())) {notifyObservers(column);if (gameBoard.checkForWin(currentPlayer.getMarker())) {gameBoard.printBoard();System.out.println(currentPlayer.getName() + " wins!");gameBoard.setGameOver(true);} else if (gameBoard.isBoardFull()) {gameBoard.printBoard();System.out.println("The game is a draw!");gameBoard.setGameOver(true);} else {switchPlayer();}} else {System.out.println("Invalid move. Try again.");}}scanner.close();}private void switchPlayer() {currentPlayer = (currentPlayer == player1) ? player2 : player1;}private void notifyObservers(int nextMove) {for (GameObserver observer : observers) {observer.update(new GameState(), nextMove);}}}
The Toss
class provides a method to perform a coin toss to decide which player goes first.
class Toss {public static Player performToss(Player player1, Player player2) {Random random = new Random();return random.nextBoolean() ? player1 : player2;}}
The GameFactory
class provides static factory methods for creating players, game boards, and games.
class GameFactory {public static Player createPlayer(String name, String marker) {return new Player(name, marker);}public static GameBoard createGameBoard(List<WinConditionStrategy> winConditions) {return new GameBoard(winConditions);}public static Game createGame(Player player1, Player player2, GameBoard gameBoard) {return new Game(player1, player2, gameBoard);}}
The GameState
class represents the state of the game, including the current game board and other relevant information.
class GameState {private String[][] grid;private String currentPlayerMarker;private String message;// Constructor, getters, and setters}
The Connect4Game
class is the entry point for the Connect Four game. It sets up the players, win conditions, and game board and initiates the game loop.
public class Connect4Game {public static void main(String[] args) {// Create playersPlayer player1 = GameFactory.createPlayer("Player 1", "X");Player player2 = GameFactory.createPlayer("Player 2", "O");// Create win conditionsList<WinConditionStrategy> winConditions = new ArrayList<>();winConditions.add(new HorizontalWinCondition());winConditions.add(new VerticalWinCondition());winConditions.add(new DiagonalWinCondition());// Create the game boardGameBoard gameBoard = GameFactory.createGameBoard(winConditions);// Create a gameGame game = GameFactory.createGame(player1, player2, gameBoard);// Start the gamegame.play();}}
The above-implemented Connect Four game system offers comprehensive functionalities for players to engage in matches, make moves, and determine win-or-draw outcomes. Employing modular design patterns like Strategy and Factory ensures flexibility to adapt to various game rules and board configurations. This design establishes a robust foundation for a scalable and adaptable Connect Four game solution, allowing customization and enhancement as needed. You can modify and further improve the design according to your specific preferences and additional features you may want to incorporate.
Object-oriented design (OOD) interviews assess candidates’ ability to solve complex problems using object-oriented principles and design patterns. Here’s a comprehensive guide to help you prepare for such interviews:
Understand OOD concepts: Learn abstraction, encapsulation, inheritance, and polymorphism for scalable and maintainable systems.
Practice problem-solving: Solve coding problems and design object models for scenarios like parking lots or banking systems.
Know design patterns: Expert Singleton, Factory, Strategy, Observer, and State patterns; apply them contextually.
System design skills: Design systems considering scalability, performance, and extensibility, and discuss decision-making based on constraints.
Effective communication: Clearly articulate problem-solving approaches and designs and be receptive to feedback.
Review past projects: Reflect on previous projects’ patterns (during discussions with interviewers), discussing encountered challenges.
Stay updated: Read about design patterns and best practices to stay informed about OOD and software design trends.
Mock interviews: Conduct realistic mock interviews to receive feedback on problem-solving and communication skills. You can visit the Educative AI-based mock interview to prepare for FAANG/MAANG object-oriented design, system design, API design, and coding interviews.
Continuous learning: Engage with the software development community and seek continuous improvement through practice.
By following this guide and dedicating time to practice, you can build confidence to excel in OOD interviews and tackle challenging design problems effectively.
The Connect Four game design presented here exemplifies the application of object-oriented design principles and patterns to create a scalable and efficient gaming system. Through the utilization of modular design patterns like Factory, Strategy, and Observer, the system ensures flexibility to accommodate various game rules and configurations. This design lays a robust foundation for a versatile Connect Four game solution, allowing for customization and further enhancement. By mastering these design principles and practicing problem-solving in similar scenarios, candidates can better prepare for object-oriented design interviews and effectively demonstrate their proficiency in complex systems.
If you’re looking to broaden your understanding and delve deeper into object-oriented design, design patterns, and SOLID principles, consider exploring the following courses as an excellent starting point and very helpful for your object-oriented design interviews:
Grokking the Low Level Design Interview Using OOD Principles
With hundreds of potential problems to design, preparing for the object-oriented design (OOD) interview can feel like a daunting task. However, with a strategic approach, OOD interview prep doesn’t have to take more than a few weeks. In this course, you’ll learn the fundamentals of object-oriented design with an extensive set of real-world problems to help you prepare for the OOD part of a typical software engineering interview process at major tech companies like Apple, Google, Meta, Microsoft, and Amazon. By the end of this course, you will get an understanding of the essential object-oriented concepts like design principles and patterns to ace the OOD interview. You will develop the ability to efficiently breakdown an interview design problem into multiple parts using a bottom-up approach. You will be familiar with the scope of each interview problem by accurately defining the requirements and presenting its solution using class, use case, sequence, and activity diagrams.
The Easiest Way to Learn Design Patterns in C#
A deep understanding of design patterns and the ability to apply them to relevant design challenges is key to writing good code. They ensure the code is easy to understand and maintain, can be extended in the face of evolving requirements, and can be reused as needed. This course will help you learn design patterns by providing context for using each design pattern and mapping it to common real-world problems that developers can relate to. It also provides hands-on code examples in C# to help apply the learned concepts. The course starts by explaining the SOLID design principles for writing good code followed by discussing the classical behavioral, creational, and structural design patterns in detail. The course also focuses on their strengths and weaknesses and the recurring challenges in software design that these patterns address. After taking this course, you can use your knowledge of design patterns and best practices to create reliable, scalable, and maintainable software projects.
Master Software Design Patterns and Architecture in C++
Software engineering researchers and practitioners noticed that parts of software projects could often be solved using approaches that were discovered earlier to solve similar problems. This led to the documentation of software design and architectural patterns, which are known solution approaches to a class of problems. This course starts by putting software patterns into perspective by answering the question, “What is a software pattern, and how is it different from an algorithm?” You will then learn about the two classes of patterns—software design and architecture patterns. Next, you’ll learn common C++ idioms. Finally, you will learn about software patterns for concurrency. By the end of the course, you will have useful and practical tools to improve the quality of your code and productivity. You’ll also be able to analyze a given part of your software project, pick the right pattern for the job, and apply it.
Free Resources