Last updated on April 11, 2024
In my original Tetris code base, I started out with a simple RotationController class. As I recall, this was a trivial class with a single shape. However, as more shapes were added, it quickly became unwieldy. Looking back at this code, I want to cringe at my lack of discipline in following proper design principles. On the bright side, I have something to blog about.
Chain of Responsibility
The chain of responsibility design pattern is known as a behavioral pattern consisting of client objects and a series of handler objects. It provides more than one object the opportunity to handle a request by linking receivers together (see left UML diagram). Instead of having one monolithic class, as I have or coupling your code to each and every concrete class, with the chain of responsibility pattern, you can avoid both.
Refactoring Rotation Controller
On the right is the UML diagram for handling user inputs in my game. The ControlListener implements the Java AWT Event KeyListener. The ControlListener listens for various keyboard actions (UP_ARROW – rotate clockwise, DOWN_ARROW – drop, LEFT_ARROW – move left, RIGHT_ARROW – move right, Q – rotate counter-clockwise, and W – rotate clockwise). When user presses UP_ARROW, Q, or W, it will create an instance of RotateController class to handle the respective rotation action. On the original UML diagram, you can see the long list of private methods I’ve implemented to handle the rotation action for various tetris shapes.
On the left is the refactored UML diagram with the rotation logic implemented using the chain of responsibility design pattern. Gone are the many private methods created to handle every possible shapes. For each shape, we now have a respective handler class responsible for rotating the shape. The chain of these rotation handler will check if can handle the rotation request by inspecting the shape associated with the request.
public Coordinate[] executeRequest(RotateRequest request) {
final Coordinate[] coordinate;
final IShape shape = request.getShape();
final Rotate rotate = request.getRotate();
if( shape.getShape() == this.getShape()) {
coordinate = getTargetCoordinates(shape, rotate);
} else {
if(this.nextHandler == null) {
throw new HandlerNotFoundException("No handler defined for " + shape.getShape());
} else {
coordinate = this.nextHandler.executeRequest(request);
}
}
return coordinate;
}
If it cannot handle the request, it will pass the request to the next handler. If it reaches the end of the chain and no handler is found, a runtime exception will be thrown. Feel free to browse the final code base on GitHub.