World Navigator web-based game.
World Navigator is a web-based multiplayer game. Players are put in a map of rooms and compete against each other to
reach the ending room and win the game. Implementing this game, I had to follow the principles in Robert Martin’s Clean
Code, Joshua Bloch’s Effective Java and the SOLID principles. I also had to use appropriate design patterns among other
things. So, here I’ll go over these points and defend my code against them.
Designing this project was not an easy feat as a lot of its components interact with each other, and there were a lot of
concurrency issues that I had to deal with. So, I had to find smart ways to meet the requirements while also trying to
defend my code against the principles I mentioned earlier. For this project, I used the Spring Framework which makes
writing stand-alone, production-grade java application with maximum decoupling and minimum boilerplate code. The Spring
Framework is built on the Model-View-Controller design pattern where a central Servlet controls the flow of the program
and offers a shared algorithm for processing requests. Here, I’ll go over each of the components in my project and give
a brief description for each of them.
These classes configure Spring to the requirements of this project and tells it how to handle HTTP and websocket
requests. They also define some beans which are components that need to be shared between objects that depend on them
and are injected to these objects on construction.
Controllers or Endpoints are where requests get handled. They deal with requests and communicate with the appropriate
service for each request.
There are two types of controllers, the first is a controller that handles HTTP GET and POST requests which are used to
load the page when a new user enters the website and for signup requests. The second is websocket controllers, after the
page is loaded, users have to login and are connected to the server through a websocket and credentials are validated on
connect requests then all user requests are handled through a RESTful API which’s enabled by the websockets and handled
by these controllers.
Services are used by other higher level components to resolve more complex requests. I have 4 services:
WebSocketAuthenticatorService:
Authenticates new websocket connect requests.UserService:
Interacts directly with the UserRepository
and is used by other components to access users.GameService:
Keeps track of all the running games and resolves requests that control the flow games like creatingPlayerService:
After a user joins a game, they’re assigned a PlayerController
Entity classes are POJOs that define the structure of the requests that’s expected from the frontend and Spring can
translate incoming requests from JSON to those model classes automatically.
In this project, I only have one repository, the UserRepository
which currently only stores the login credentials for
users but can be used in the future to create a profile for each user and track their games, wins, loses, etc.
The UserRepository
is a Mongo repository which is automatically configured by Spring.
The AuthChannelInterceptorAdapter
class intercepts new websocket connect requests and forwards them to
the WebSocketAuthenticatorService
which checks the user credentials and tries to match them with user credentials from
the UserRepository
. If the credentials provided by the user in the request don’t match any credentials in the
repository, the connection is rejected.
The game was designed as an API that’s completely decoupled from the layers above it. The game has many parts with
different responsibilities so we’ll discuss each of them:
The Game class can be thought of as a Mediator as it controls the interaction of the different components
of the game and controls the flow of the game. We’ll discuss each of the components it interacts with separately.
Door.getNextRoom()
without having toLock
class andLock
object. So, when one of the doors gets unlocked for example the other door will beThe game requirements state that each the map should at least have 50 rooms and that there shouldn’t be a limit for how
many players can join a game so, a map generator that creates maps randomly was needed. The map generator consists of
multiple class:
WorldMapGenerator:
The main class which connects all of the components the map generator and defines the algorithm.DifficultyLevel:
This interface defines the variables in the map generation and the probabilities of certain eventsDifficultyLevel
, the DefaulyDifficultyLevel
.GameRandomizer:
The class is the only one that interacts directly with the specified DifficultyLevel
. It gives aEntityGenerator:
This class is concerned with generating entities that could be placed in the rooms. Currently, theObservables
and it handles generating those by having a list of random generators, oneObservables
and when a new one is requested it picks one randomly and generates aObservable
.GameBuilder:
The game builder isn’t part of the generator package as it could be useful for other things like aThe player is at the center of the game. It interacts with almost all of the components in the game. So, managing it in
a clean way is really hard as it was really easy for it to become overcomplicated. I started by making a manager class
for each type of operations the player could do. The player class has seven managers which each can manage certain type
of operations. And two other class, Inventory and Location, which define the state of the player and are shared between
managers so they can manipulate them.
The Interface:
Now I needed a way to let other classes interact with the managers so at first the player class acted as a
Facade. I defined a method for every operation that could be requested from the player in the player class and
it would redirect that request to the appropriate manager but that got redundant really quick because, as mentioned
before, there are a huge number of operations a player can do and the player class started turning into a god object.
So instead, I exposed all of the managers through methods and classes that use the player class need to call the
appropriate manager in method calls like this player.navigate().turnRight()
.
This increases the coupling between components of the game as manager might be though of as implementation details but
this allowed to make the player class a lot simpler and easier to read.
Commands:
I needed a way to decouple the player class from the outer classes because each user gets assigned a player object
when they join a game and the PlayerService
directs each requests from the user to their player object. Moreover, I needed a way to check if the player is in the
correct state to execute a command and adding a check statement in each method isn’t a great solution. So I used a
combination of the Command pattern and the State pattern by creating a command object
for each command which acts as a decoupling layer between the player class and the player service and each command
defines the state it could be executed in. I then created a PlayerController
class which could take commands and
execute them making the Player
class totally decoupled from the PlayerService
. It also puts the commands in a
queue and executes them one by one making sure no concurrency issues can happen as requests from the user could arrive
asynchronously.
The Inventory:
The Inventory has to hold various kinds of items. Currently the game has Flashlight
, GoldBag
and Key
which are
considered to be inventory items, Each of which are stored inside the Inventory
class separately. So adding and
accessing those items from the Inventory
is dealt with differently for each type. So, I decided to go with
the Visitor design pattern here as it serves the purpose well.
The game requirements state that if two players walk into the same room, they have to get into a fight. It also needs to
check if a player enters a winning room and wins the game. So, the game needed a way to keep track of who’s moving
where. So, players have to request move operations from the game so it could do the needed checks. I created
a FightTracker
which keeps track of who was the last person to enter each room so if someone else enters it, it’ll
know and it creates a new Fight
between the two players. For the case where three or even more players enter the same
room, the FightTracker
links Fight
objects together so that when a fight ends, it notifies the one after it with the
winner of the fight.
For this book, I’ll go through the points that apply to this project from chapter 17 Smells and Heuristics
and defend
my code against them. A lot of them are self-explanatory so I’ll just list them without description.
check
command I could have made it such thatsellItemToPlayer()
in the Seller
class whichGoldBag
of the player as an argument and takes the price of the sold item from it. But I decided to do this soSeller
class can verify that the player has enough gold to complete the purchase instead of just “trusting” theG5: Duplication
G6: Code at Wrong Level of Abstraction
Defending your code against this point isn’t an easy task as there are many ways it could go wrong. I tried my best to
satisfy it by using interfaces wherever it’s possible and using design patterns that help with that.
Player
class has a very intensive interface like explained earlier but that’s necessary because it matches theThese are in place to check that the program is running correctly and to make sure that these components are used the
public Room getNextRoom() {
if (canPassThrough()) {
return nextRoom;
} else {
throw new ItemIsLockedException();
}
}
@AllArgsConstructor
Inventory
and Location
a lot.Player
class simple.isLocked()
method from the LockedWithKey
abstract class in a Transitive NavigationisUnlocked()
and replaced all of those calls with it.J1: Avoid Long Import Lists by Using Wildcards
Google code style guide prohibits the
use of wildcard imports so I decided to not follow this principle.
J3: Constants versus Enums
I avoided using constant integer codes even though they are attractive to use. In the “Karel” assignment I represented
directions with integer constants and which then I could do mathematical operations on to modify. For example
the turnLeft()
method looked like this:
public void turnLeft() {
direction++;
direction %= 4;
}
But now I realized that this is way less readable. I even had to put a comment on it to explain it, which proves the
point here. So, this time I used an Enum to represent directions instead.
I tried to be very mindful about my names. I made sure my names are descriptive and express exactly what a function does
or a variable represents without using any encodings. My names aren’t perfect by any means and I still got a lot to
learn but I think I made a lot of progress compared to the names I used to choose in the past.
This book presents a lot of useful tools that improve Java programs and make them a lot more expandable and reusable.
Here is a list of the items I applied in my code:
Key
class where I need to keep track ofMapBuilder
because maps are complicated and have to constructed in many steps.ResourcesManager
a singleton with a static factory method to make sure there’s only one instance of it inKey
class by providing static factory methods to ensure the codePlayerService
GameService
need to remove these objects so they can be collected by the garbage collector. That was doneGame
and Player
classes notify the services above them when they need to be removed.Item 10: Obey the general contract when overriding equals
I needed to override the equals method in multiple classes and made sure it doesn’t violate any of the points
mentioned in the book.
I used this format for all of them:
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
Player player = (Player) obj;
return Objects.equals(id, player.id);
}
LockedWithKey
and KeyHidingSpot
because they provide an implementation thatRoom
class as at first I had a boolean variable lightSwitch
indicating whether the roomRoomWithLightSwitch
and removed the boolean variableRoom
.ItemIsLockedException
which could get thrown when a player requests to moveI made sure all of my classes only have one job and one reason to change which decreases the coupling of the system and
makes adding changes to it a lot easier and a less complicated. It also makes the code more readable and easier to
understand because it creates a level of abstraction at each layer.
I did that by breaking down large classes to smaller classes so that each can handle one of the responsibilities of the
original class. Also by breaking down large methods down to smaller ones as well, with each of them operating on a
single level of abstraction.
Some design patterns helped with this as well like the Mediator and the Facade design patterns.
This principle isn’t easy to apply perfectly because for it to be applied programmers have to try and predict possible
future changes to the code and resolve them by introducing some level of abstraction that would make adding these
changes easy and not require editing existing code.
The best way to apply the OCP is through polymorphism removing the coupling between components and allowing the addition
of new ones that can fit seamlessly in the system.
This is best demonstrated in my player manager classes as they avoid interacting directly with the map items and instead
through interfaces that are meant to bring out a single property of the object. These interfaces are:
Observable
which is implemented by items that can the look
command can be applied to and it includes all of theCheckable
which is implemented by items that the check
command can be applied to.PassThrough
which represents items that can move players between rooms and currently limited to the Door
class.Container
which represents items that can hold a set of InventoryItems
and currently it’s only implemented byChest
class.And a couple of abstract classes that help achieving the same goal as well:
KeyHidingSpot
which represents items that can hold a Key
object, implemented by the Painting
and Mirror
LockedWithKey
which represents items that can require a Key
to be accessed, implemented by the Door
and Chest
This principle defines that a superclass should be replaceable with its subclasses without breaking the program. It
implies that an overridden method shouldn’t be restrictive than the super method.
This can be demonstrated in the RoomWithLightSwitch
class which extends the Room
class. The RoomWithLightSwitch
doesn’t take away the ability to see items in the dark with a Flashlight
but it adds the option of turning the light
switch to see items without a Flashlight
.
Robert Martin defines this principle with “Clients should not be forced to depend upon interfaces that they do not use”
.
I made sure apply this by having each interface represent a represent a single property and making it include only
methods that are essential for that property.
According to Robert Martin Dependency inversion consists of these parts:
I applied this by defining an interface of methods for each component that other components can interact with it
through. This means the inner implementation of a component can be changed without it affecting the rest of the system.
It also means a component can be easily reused in other parts if the system.
Writing this project, I decided to follow
the Google Java Style Guide.
First off, I installed the google-java-format plugin on Intellij which did most of the work for me. Ensuring I have the
right indentation, optimizing the imports, keeping the column limit, etc. But there are a couple of things that the
Plugin couldn’t fix for me so I had to make sure to follow some of the rules, like:
if(keys.containsKey(keyName))return keys.get(keyName);
else return createKey(keyName);
To this:
if(keys.containsKey(keyName)){
return keys.get(keyName);
}else{
return createKey(keyName);
}
Key
class to RESERVED_NAMES
Utilization was a great focus in this project. The project was designed to serve request as quick as possible and have
the least amount of thread blocking. That was achieved by using optimized algorithms, appropriate data structures and
also by queuing player commands and serving them synchronously. Also by using a ThreadPoolTaskScheduler
which’s shared
throughout the code which allows achieving maximum utilization.
As for scalability, horizontal scaling can’t be applied because I’m using a simple in-memory message broker which means
if a player connects to an instance, other instances can’t interact with them, and by the nature of the game this is a
problem. But, it can be easily fixed by using a more sophisticated message broker service like RabbitMQ or ActiveMQ
which could run independently and would allow instances to send messages to users that have their websocket connection
on other instances.
I mainly used three types of data structures in my project so I’ll list them and explain why I decided to use each of
them:
Inventory
where items need to added and removed continuously.Trader
classEnumMap
that allows the use of enum type keys. I used it in the Room
Direction