It has been said that design patterns is a symptom of language deficiency. If you need a pattern that indicated a language feature should have existed instead.
Here I'd like to show how many class design patterns can be simplified in the Go programming language to look less like patterns and more like language features. This post was inspired by previous discussions online (Joe Gregorio) about how design patterns were rarely talked about in the Python community, due to the simple fact that in dynamic languages, the need for specific patterns isn't really there. The language often supports directly what you simulate with a pattern. The examples I will use will mostly be related to computer games. Meaning examples will be about classes and objects you typically find in a game engine
Command Pattern
Let's say we have a game editor which lets us place sprites in a game world.
The code below shows how we might imagine this could be done in C++. When the user click on a position, that position pos is given to a function along with the sprite which will be placed there.
class Command {
virtual void Do() = 0;
virtual void Undo() = 0;
};
class PlaceSpriteCmd : public Command {
Sprite* _sprite;
Point _oldpos, _newpos;
public:
PlaceSpriteCmd(Sprite* sprite, Point pos) :
_sprite(sprite), _newpos(pos)
{
_oldpos = sprite->position();
}
void Do() {
sprite->setPosition(_newpos);
}
void Undo() {
sprite->setPosition(_oldpos);
}
};
static CommandStack _undoStack, _redoStack;
void PerformUndoableCmd(Command *cmd) {
cmd->Do();
_undoStack.push(cmd);
}
void UndoLastCmd() {
Command* cmd = _undoStack.pop();
cmd->Undo();
_redoStack.push(cmd);
}
void onPlaceSprite(Sprite* sprite, Point pos) {
Command *cmd = new PlaceSpriteCmd(sprite, pos);
PerformUndoableCmd(cmd);
}
The code is written with clarity in mind, so it is not meant to compile. It has no error checking, would probably leak memory etc.
It should thus be clear that supporting Undo in C++ require us to:- Create a Command base interface.
- For each command we need a concrete subclass of this interface.
- Three methods have to be implemented at least in these subclasses: Constructor, Do and Undo.
- We need to define some member variables in concrete subclass to store enough information to be able to perform the undo. E.g. in the case above we store the both current and new position to support undo and redo.
Since Go supports closures, functions are essentially first class objects which is in many ways what we simulate with Command subclasses, this becomes a lot simpler:
func MakePlaceSpriteCmd(sprite *Sprite, newpos Point) (undo, redo func()) {
oldpos := sprite.Position()
undo = func() {
sprite.SetPosition(oldpos)
}
redo = func() {
sprite.SetPosition(newpos)
}
return
}
type Command struct {
undo, redo func()
}
var undoStack, redoStack CommandStack
func PerformUndoableCmd(undo, redo func()) {
redo()
undoStack.push(Command{undo, redo})
}
func UndoLastCmd() {
cmd := undoStack.pop()
cmd.undo()
redoStack.push(cmd);
}
func onPlaceSprite(sprite *Sprite, pos Point) {
undo, redo := MakePlaceSpriteCmd(sprite, pos)
PerformUndoableCmd(undo, redo);
}
It might not look a lot simpler, but that is mainly because of a certain fixed amount of code that has to be both places. What should hopefully be clear from the example however is that the difference becomes more noticeable as one starts adding commands. In the C++ case that involves creating a whole class.
Strategy Pattern
The main motivation behind the strategy pattern is to avoid an explosion in number of subclasses and code duplication. E.g. consider a real time strategy game with sprites for Allied tanks, Axis tanks and Soviet tanks. All the sprites are rendered differently, and might have different amounts of armor, gas tank volume etc. The tanks might have different behavior like cautious, aggressive, sneaky etc. The naive approach would be to create a subclass of the allied tank for each of those behaviors. However we would have to do this for each tank type even though the behavior code might be exactly the same. This leads to an explosion in subclasses.
Hence the use of the strategy pattern, which involves encapsulating the behavior into a separate objects and assign different behavior objects to tank objects at runtime. The previous post on Go vs Java touches upon this with the discussion of steering behaviors. Essentially steering behaviors could be thought of as the same as behaviors for sprites. The Go vs Java example shows how a language like Go with first class functions simplifies greatly the creation of strategy objects, in much the same way as we demonstrated in the Command pattern section above.
In Java and C++ we would have needed to create subclasses such as Agressive, Sneaky and Cautious of an interface, say Behavior. Go gets away with defining closures.
We could even apply the strategy pattern to how sprites are drawn:type Sprite struct {
pos Point
draw func(pos Point)
}
func (sprite *Sprite) Draw() {
sprite.draw(sprite.pos)
}
Factory Pattern
In the Design Patterns book by the Gang of Four, different construction patterns are demonstrated using the example of a maze. The idea is that a maze consists of a number of connected rooms of different types. The reason for using some kind of factory is that we might want to change the types of rooms used but we don't want to have to change the code that creates the maze by instantiating room objects and connecting them. Here is an example of what the maze construction code might look like in C++:
void BuildMaze(RoomFactory* factory) {
Room* a = factory->CreateRoom("Prison");
Room* b = factory->CreateRoom("GuardRoom");
Connect(a, b);
}
In Go we don't need to use an instance of a class at all. It is more convenient to simply use a function object:
func BuildMaze(createRoom func(roomType string) Room) {
a := createRoom("Prison");
b := createRoom("GuardRoom");
Connect(a, b);
}
But in practice it goes beyond this. Those familiar with more dynamic OOP languages like Smalltalk and Objective-C know that classes are objects and can thus be passed around in the code. In languages like C++ and Java we usually simulate this by creating a factory class for each class we want to be able to pass around as if the class was an object.
In Go we can achieve much the same without extra code. The idiomatic way of creating an instance of a struct is to call a free function. This is because Go does not support constructors. So instead of coding constructors to instantiate structs you code free functions. The beneficial side effect of this is that because functions are first class objects, these creation functions can be passed around in similar fashion as Objective-C class objects. Constructors are not regular functions and can thus not be passed around in this fashion. Thus separate factory functions or classes needs to be created in languages like Java and C++.
Conclusion
While writing this blog entry, I tried to compare Go with Python. It became apparent that while Go can achieve a lot of the same simplicity as Python, it still can't do all patterns as simple as in Python. The most problematic pattern I noticed was subject-observer. In Joe Gregorio's presentation on the lack of design patterns in Python, it is written as this:
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def scale(self, n):
self.x = n * self.x
self.y = n * self.y
def notify(f):
def g(self, n):
print n
return f(self, n)
return g
Point.scale = notify(Point.scale)
p = Point(2.0, 3.0)
p.scale(2.5)
Which can't be easily reproduced in Go. However the other patterns mentioned by Joe Gregorio: Iterator, Factory and Strategy should be just as easy as I hope I demonstrated clearly here. Another aspect which I haven't seen anybody write about in detail is lack of refactoring in Python and Ruby code bases. I am not saying it doesn't happen but I notice a couple developers who develop in both C++, Java and Ruby point out that they almost never refactor their Ruby code, while it is frequent occurrence in Java and C++. The reason being given that the need seldom seem to arise. I think it would be interesting to see in the future when more experience is gained from using Go, what the experience will be for Go. Will there be a lot of design patterns and refactoring or will the experiences be more similar to that of Python and Ruby developers?
1 comment:
thanks for this information .
you can find factory design pattern in simple and easy to understand language in a detail on
http://www.itsoftpoint.com/?page_id=2666
Post a Comment