views:

167

answers:

4

How would you adhere to the "Tell, don't ask" principle (henceforth "the principle") in the following simple scenario? In a Tetris game, I have Board, BlockGrid and Piece classes relevant to the following example:

public class Board
{
    private var fallingPiece:Piece;
    private var blockGrid:BlockGrid;
    ...
    public function moveFallingPiece(xDirection:int, yDirection:int):void
    {
        blockGrid.movePiece(fallingPiece, xDirection, yDirection);
    }
} 

Once fallingPiece is placed in the bottom row of BlockGrid, it should no longer be the "fallingPiece". Am I right in that I'm not violating the principle with the following?

if(blockGrid.getPiecePosition(piece).y == 0)
{
    fallingPiece = null;
}

But is that really different from this, which I think clearly violates the principle?

public function moveFallingPiece(xDirection:int, yDirection:int):void
{
    if(blockGrid.getPiecePosition(piece).y > 0)
    {
        blockGrid.movePiece(fallingPiece, xDirection, yDirection);
    }
    else
    {
        fallingPiece = null;
    }
}

I'm not assuming that I've designed these class relationships in the proper way to work with the principle. Please advice on an alternate design if that's what I'm missing.


EDIT, Proposed solution:

I went with the answers proposing "command feedback" via events. Board tells BlockGrid to move a piece. BlockGrid's movePiece method dispatches MOVED_TO or MOVE_FAILED events depending on the result, which Board can listen to and use to determine whether a piece has stopped falling. Please don't hesitate to provide feedback on this solution.

public class Board
{
    ...
    public function Board()
    {
        ...
        blockGrid.addEventListener(PieceMoveEvent.MOVE_FAILED, onPieceMoveFailed);
        ...
    }

    public function moveFallingPiece(xDirection:int, yDirection:int):void
    {
            blockGrid.movePiece(fallingPiece, xDirection, yDirection);
    }

    public function onPieceMoveFailed(event:MovePieceEvent):void
    {
        if(event.instance == currentlyFallingPiece && event.fromPosition.y != event.toPosition.y)
        {
             currentlyFallingPiece = null;
        }
    }
+1  A: 

I think, to better follow the Tell, Don't Ask principle, you should have blockGrid notifying your Board class when fallingPiece has reaches it's resting point. In both scenarios above, you are asking blockGrid if the piece's position.y == 0 in order to determine whether or not fallingPiece should be null. Instead, you want blockGrid to tell the Board class that fallingPiece.y has hit 0.

sberry2A
actually the pieces shouldn't have any "logic" in them, the board should tell them what to do.
fuzzy lollipop
A: 

I would expect a class representing each shape (without position information), a controller containing a shape, position and orientation, and another class representing the current resulting grid of "landed" shapes. The landed-grid would have a

testLanded(shape, shapePosition, orientation)

method which would be called before/after each move operation to decide if the shape is to join the landed grid or should move and stay as the falling piece.

I'm going on the idea of not giving data to objects that shouldn't really own that data - but I've never implemented Tetris...

DaveC
Interesting, but how would you implement testLanded without querying other objects for their state?
Stiggler
the landedGrid would likely have a 2d array of where previous blocks have landed - a block would stop being a proper block at this point and just become some colours in that grid. Thus the landed-grid consumes the blocks into its own data structure, so it has all the information to hand to tell if the current block has landed on the landscape left by previous blocks
DaveC
+1  A: 

What you are looking for is Event driven programming. You need a Listener interface with a method called .event() and an Event interface to represent the events. Objects will register with other objects ( callbacks ) to the Listener interface.

when you create a Piece and Board they should implement the Listener interface. Then you can set the Board with registerListener(board); Then when things happen inside Piece it will loop thru all the registered listeners and call .event(event) on each. Same with the Board, call board.registerListener(piece) each time you create a new piece, as it decides things are happening it can tell all the registered listeners what has happened. Then you can tell a piece it is no longer falling by the Board object deciding this. Here is the obligitory Wikipedia entry.

fuzzy lollipop
A: 

You may need to rethink your design. Does Board really need to track the falling piece or should that belong to BlockGrid? Iron out who owns what behavior.

Keep position information on your Piece class and possibly have your Piece class hold an instance of the BlockGrid.

You can then try something like this in your Board class...

public function moveFallingPiece(xDirection:int, yDirection:int):void
{
    blockGrid.moveFallingPiece(xDirection, yDirection);
}

Then in BlockGrid's moveFallingPiece method...

public function moveFallingPiece(xDirection:int, yDirection:int):void
{
    fallingPiece.move(xDirection, yDirection);
}

In Piece's move method, add your logic...

public function move(xDirection:int, yDirection:int):void
{
    setPosition(xDirection, yDirection);

    if (getPosition().y <= 0)
    {
        blockGrid.setFallingPiece(null); 
        // this can bubble up to Board if need be
    }
}

Not sure of all the power of AS3, but it would make sense to use abstractions here. (i.e., have your Piece class depend on ITrackFallingPieces instead of BlockGrid and have BlockGrid implement ITrackFallingPieces).

Good luck!

Kevin Swiber
Thanks, I appreciate the design feedback! However, I don't know if it answers my question; calling blockGrid.setFallingPiece from within Piece seems like poor encapsulation. As far as the design goes, I decided against storing position information on Piece because that would allow the possibility of two Pieces having the same position. BlockGrid was intended to keep track of Piece positions on a grid (possibly better named PieceGrid); the concept of "falling" seemed like a responsibility too many.
Stiggler
Man, we are talking about a Tetris game here, not the lattest real time NASA fuel balancer. Cool down.
e-satis
Who's that comment aimed at? My main intent with the question is not get a Tetris game to work, but to better understand a design principle...
Stiggler