What is the benefit of using immutable objects?
I'm making my Tetris clone and I have a Piece class. Now for the rotations I could have it change its internal data that represents its body, or I could make it immutable and have several objects that each represent a certain piece in a certain orientation.
I'm not sure what the cost/benefits of each option are.
You would be better changing the internal data (state) of the piece. Think of the piece as an object. To turn the piece around you don't replace it entirely, you're just changing which way it's pointing. I can't see any benefits in doing it the other way. In fact it would seem to me to be fairly messy.
CYer, Blitz
"Immutable" has some other connotations here, too awf - there is a C++ keyword, as shiva points out called "mutable". I'd describe what you seem to be talking about as "const objects" to avoid confusion here. It's been quite a while since I did any java, so I can only speak with certainty about the C++ side (but the same concepts apply to java)
Anyway, this is kind of a mix of the two ideas...
I guess the first thing I'd do here is to look at the Piece class - I find it a bit hard to explain the exact logic I'd use (I tend to do things intutively with design now, so it's hard to describe) - but I'd instinctively break the Piece class into two - one representing the shape of the object, and one representing the other state information about the Piece.
The Shape itself wouldn't know about its absolute orientation - it's up to the Piece object to provide context on this.
When the Piece class is queried about what its shape is, you'd really have a few options here:
* if you're going for 3D pieces, then the Piece would return the orientation information (be that a euler angle, matrix or quaternion) and return the Shape it holds a pointer/reference to.
* for 2D, you could copy the 3D version and have an angle returned, or
* if you're going for 2D pieces, then you could copy the Shape from the Piece class, rotate it, and store the rotated version on the Piece class (this is an excellent spot for the mutable keyword BTW - but more on that soon)
For 2D I'd probably recommend the last option as it simplifies the code where you're actually using the shapes (eg testing if they collide or drawing them)
So the places const crops up here would be:
(A) the "original" Shapes are const with respect to the Pieces (ie Pieces are not allowed to modify them, but you need to be able to delete them later)
(B) the "duplicated" Shapes are not const, but their copy constructor would take a const ref to the Shape they're duplicating
(C) the "duplicated" shape - it makes sense to store this on the Piece until the orientation of the piece changes, but you want to return the "duplicated" shape as a const pointer/reference.
As usual, it's easier to produce some code than describe it properly :-) so I've put some code at the end of this to help you see where A, B and C are.
The point (C) is where the mutable keyword comes in - when a data member is declared as mutable then you're allowed to alter that data member from within a const member function. You should use this keyword sparingly, and pretty much only in the case I've outlined below (for what is called "lazy evaluation"). Pretty much anywhere else I've seen mutable used is usually a total hack.
[Note 1 that applies to the code below: It's good practice to order your data members in order of classes, pointers and references, then decreasing float/int sizes - this will reduce the size of the object - so the mrsShape and mrsActualShape would usually be best ordered the other way around]
Cheers,
Mark
[code]
class Shape
{
public:
Shape(/*info on the actual shape would go here*/);
Shape(const Shape &rtShape) { /* copy the bits needing copying, clear the rest */ }
Shape & operator=(const Shape &rtShape) { /* copy the bits needing copying, clear the rest */ }
// or perhaps you might just go for just one rotate(int angle) - this modifies the internal state of
// the shape
void rotateRight() { /* do whatever's appropriate for your representation of the shape */ }
void rotateLeft() { /* do whatever's appropriate for your representation of the shape */ }
private:
// whatever representation you choose
};
class Piece
{
public:
//////////// (A) - original shape is const with respect to the Piece ////////////
Piece(const Shape &rsShape) : mrsShape(rsShape), mbRotationDirty(false)
{
}
// Make the member function const as we don't need to change state of the
// object to get the rotation...
int getRotation() const { return miRotation; }
void setRotation(unsigned int iRot)
{
// Ensure rotation is < 360
miRotation = iRot % 360;
// Remember that the mrsActualShape is no longer valid by setting a flag
//
// This is used for "lazy evaluation" - we _could_ do all the rotations of our
// copy of the shape here, but perhaps we're going to adjust the orientation again
// before we need to know the final Shape to use/draw
mbRotationDirty = true;
}
// Make this routine const because this isn't changing the "core" state of the object
const Shape & getShape() const
{
// If the rotation hasn't changed since last time we were in this member function,
// then it's safe to use our cached copy of the rotated shape
if (!mbRotationDirty)
return mrsActualShape;
// By using the mutable keyword down below against the "mrsActualShape" member
// we allow ourselves to change the member from within a const member function
//////////// related to (B) - duplicated shape is not const ///////////////////////
//////////// related to (C) - use of the mutable keyword //////////////////////////
mrsActualShape = mrsShape;
switch (miRotation)
{
case 0:
// do nothing, mrsActualShape is already in the right orientation
break;
case 90:
mrsActualShape.rotateRight();
break;
case 180:
mrsActualShape.rotateRight(); // probably want to do it all in one go
mrsActualShape.rotateRight(); // but this is just for example
break;
case 270:
mrsActualShape.rotateRight(); // probably want to do it all in one go
mrsActualShape.rotateRight(); // but this is just for example
mrsActualShape.rotateRight();
break;
}
mbRotationDirty = false;
return mrsActualShape; // Implicitly converted to const Shape &
}
private:
//////////// (A) - shape is const with respect to the Piece ////////////
// (see Note 1 regarding order of this and the following member)
const Shape & mrsShape;
//////////// (B) - duplicated shape is not const ///////////////////////
//////////// (C) - use of the mutable keyword //////////////////////////
// (see Note 1 regarding order of this and the previous member)
mutable Shape mrsActualShape;
// What rotation is the
unsigned int miRotation; // 0, 90, 180, 270 degrees
//////////// (C) - use of the mutable keyword //////////////////////////
mutable bool mbRotationDirty;
};
[/code]
(Apologies for the 4-space tabs being converted to 8-spaces!)
I think I get it. A piece object contains two shape objects. One const and one mutable. The const shape is the original orientation (one of the seven tetris types) and is never changed.
The mutable shape is what you would return after a rotation. The mutable is like the current orientation of the piece. So this is changing the internal state.
Not sure about lazy evaluation and why you would need to rotate by more than 90. Wouldn't each time through the game loop have only one rotation? Is lazy evaluation for generating each rotation as a new piece object? If so then that seems to be like the many objects scenario.
Thanks for all the help.
Awf, yeah that's correct what you've said.
I mainly went for the lazy evaluation to explain when to use the mutable keyword - but yes, you could well remember the rotated object's shape and rotate by 90 degrees when the user rotates the
piece.
The reason I'd shy away from not recalculating the new Shape from the original one is that if you used the same idea with a 3D program, you would have to worry about numerical error building up.
As you're suggesting, rotating by 90 degrees would work fine for a 2D tetris game.
The lazy evaluation is of the new Shape from the original Shape. So in the code, the "mrsActualShape" is being lazily evaluated. Yup, you're right that it's like the many objects scenario - but you're also changing the actual shape's internal data.
Cheers,
Mark
by immutable are you referring to const?
if so, the benefit of a const object is that it is only able to invoke const methods, so your data can not be modified accidently.
if you have members which need to be modified, such as your piece orientation, then you can use the mutable keyword for these.
edit: the above is assuming c++, if its java (which im thinking right now it might be), then the concepts are the same i believe, though im not entirely positive