I am designing a game with multiple levels. I have a setup class that sets up the board based on the argument it receives, which indicates which level it should set up. Here is the class:
public class BoardState {
public BoardState(InitialState state) {
switch (state) {
case EMPTY:
setupEmptyState();
break;
case INTEGRATIONTEST:
setupIntegrationTestState();
break;
case LEVEL_1:
setupLevelOne();
break;
case LEVEL_2:
setupLevelTwo();
break;
default:
throw new Error("Invalid level selection");
}
}
private void setupEmptyState() { }
private void setupIntegrationTestState() { }
private void setupLevelOne() { }
private void setupLevelTwo() { }
}
This works fine, but every time I add a new level I have to add code in three places: The InitialState
enum
which defines the list of accepted states, the switch
statement in the constructor, and the body of the class, where I have to add a method to set up the level in question.
One nice thing that I want to keep is the fact that my GUI automatically populates with a new button for each level I add based on the enum
defining the list of levels.
How can I refactor this code so that there is less overhead associated with adding a new level?
You could use inheritance, polymorphism is the keyword here.
Set up your InitialState
class as abstract base class (or interface if you have no common fields) and define a method public abstract void setup();
.
abstract class InitialState {
public abstract void setup();
}
Then, for each of your original switch cases, derive a class from your base class, for example LevelOne
, and implement its specific by overriding setup()
.
class LevelOne extends InitialState {
@Override
public void setup() {
// The code from "setupLevelOne()" goes here
}
}
Your BoardState
class reduces to this:
public class BoardState {
public BoardState(InitialState state) {
// At runtime, the right method of the actual
// state type will be called dynamically
state.setup();
}
}
However, if you need to set interal state of your BoardState
class, consider defining the setup method as public abstract void setup(BoardState boardState)
, so you can access its getter and setter methods.
This appraoch could also foster reuse of code, as you could add several abstract layers for different types of levels.
Often when you need to reduce code duplication, an interface arise. This time (based on your comment in OP) it seems you need to add different objects to the board depending on which level you are:
import java.util.List;
public interface LevelSettings {
List<GameObject> startingObjects();
}
Now, BoardState
looks like that (no more setupX()
methods)
import java.util.List;
public class BoardState {
private final List<GameObject> gameObjects;
public BoardState(LevelSettings settings) {
this.gameObjects = settings.startingObjects();
}
}
Since you also specified it is nice for you to have an enum
to dynamically creates buttons on the GUI, one can combine the best of both world (interface and enum) by implementing the interface in an enum...
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public enum InitialState implements LevelSettings {
EMPTY {
@Override
public List<GameObject> startingObjects() {
return Collections.emptyList();
}
},
INTEGRATIONTEST {
@Override
public List<GameObject> startingObjects() {
GameObject g1 = new GameObject("dummy 1");
GameObject g2 = new GameObject("dummy 2");
return Arrays.asList(g1, g2);
}
},
LEVEL_1 {
@Override
public List<GameObject> startingObjects() {
//read a config file to get the starting objects informations
//or also hardcoded (not preferred)
}
},
LEVEL_2 {
@Override
public List<GameObject> startingObjects() {
//read a config file to get the starting objects
//or also hardcoded (not preferred)
}
};
}
And that's it basically. If you need to add LEVEL_3
do it in InitialState
and everything will follow.
From here it goes beyond what you requested, feel free to ignore this part if you are not convinced.
As a good practice I would store these configurations only in config files to reduce even more the code duplication and gain in flexibility:
import java.util.List;
public enum InitialState implements LevelSettings {
EMPTY {
@Override
public List<GameObject> startingObjects() {
return readFromFile("empty.level");
}
},
INTEGRATIONTEST {
@Override
public List<GameObject> startingObjects() {
return readFromFile("integration_test.level");
}
},
LEVEL_1 {
@Override
public List<GameObject> startingObjects() {
return readFromFile("1.level");
}
},
LEVEL_2 {
@Override
public List<GameObject> startingObjects() {
return readFromFile("2.level");
}
};
private static List<GameObject> readFromFile(String filename) {
//Open file
//Serialize its content in GameObjects
//return them as a list
}
}
So that when you decide to add a new level you actually only need to know the filename in which the level's configuration is stored.
What you will see there is really tricky and I don't advice you to use it in production code (but it reduces code duplication) !
import java.util.List;
public enum InitialState implements LevelSettings {
EMPTY, INTEGRATIONTEST, LEVEL_1, LEVEL_2;
@Override
public List<GameObject> startingObjects() {
return readFromFile(this.name() + ".level");
}
private static List<GameObject> readFromFile(String filename) {
//Open file
//Serialize its content in GameObjects
//return them as a list
}
}
Here we rely on enum names themselves to find the corresponding correct file. This code works because it is based on the convention that the files are named accordingly to the enum names with the ".level" extension. When you need to add a new level, just add it to the enum and that's it...
well you can refactor all of the work into a one method. say it takes an int as the ID of the level, while it loads a JSON file containing structured information of each level, and creates the given level. for example :
"levels" : [
"level" : {
"id" : "001",
"size" : "200",
"difficulty" : "2"
},
"level" : {
"id" : "002",
"size" : "300",
"difficulty" : "3"
}
]
then, in your code:
public void setupLevel(int id) throws levelNotFoundException{
//somehow like this
Document doc = parse("levels.json");
for(element elm: doc.get("levels")){
if(Integer.parseInt(elm.get("id")).equals(id)){
//setup your level
}
}
}
and then somewhere you call your method:
int levelId = getNextLevel();
try{
setupLevel(levelId);
} catch (LevelNotFoundException e){e.printStackTrace();}
or you can use XML, or simply hard code it, and store all levels in an array
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With