Getting With the Program

Building a game ± or any application ± in NeXTstep is a winning proposition

by Charles L. Perkins

Two common myths surround object-oriented programming: the first is that it takes years of training to do it properly.  The second is that object-oriented programming is no more than traditional good programming practices dressed up.  Both are wrong.

Object-orientation is a new way of looking at problems, a new point of view. As such, traditional programmers can't just use it without guidance.  But it doesn't take years ± or even months ± to begin using it.
 
In one sense, object-oriented programming is as old as programming itself. It codifies clever strategies hard won over the decades into a new, coherent whole. It goes beyond those strategies, however, to suggest new ways of viewing the process of programming and of its result.  [See ªObject Lessons,º NeXTWORLD  Spring 1992, for some history and background on object-oriented programming.]

Like any good programming paradigm, the object-oriented approach promises to help programmers simply and quickly create programs that are easier to read, comprehend, maintain, and reuse.  These promises translate into four key goals:

Modularity. Programs are broken into small, comprehensible pieces that can be easily combined and reused in later programs.

Factoring. Each parameter and code fragment appears in one and only one place, to ease modification and later enhancement.

Encapsulation. Information in the program is kept near the parts of the program that manipulate that information, making it easier to change its representation.

Abstraction. Implementation details are kept hidden from the higher levels of the program, so later changes at lower levels do not affect the rest of the program.

Object-oriented programming, though, differs in the approach it takes. Rather than requiring separate language facilities to achieve each of these goals, an object-orientated language's basic elements perform double duty, allowing programmers to restructure the way they think (and solve) their problems.

What is an object, anyway?

The fundamental unit of an object-oriented program is the object. Objects contain all the information about one small part of the program and all the procedures allowed to manipulate that information. This achieves part of the modularity, factoring, and encapsulation goals right away.

An object is also an instance (or member) of a class, which describes all the common behaviour of its members. For example, NeXTstep objects in the class called Window display information on the NeXT computer's screen. Each window on the screen is represented by a different member, or instance, of the Window class.

Every class defines a set of messages that objects of that class can receive. Simple messages, like commands, tell objects what to do. If the programmer sends an orderOut: message to a window, for example, the window takes itself off the screen. Sending the window the message orderFront: puts the window on top of all of the other windows on the screen. Inside the object, a procedure (called a method) acts to carry out the message.

Much of an object-oriented program consists of objects sending messages to each other, and returning other objects as the result of those messages. Since all the information is inside objects, and the only access to that information is through messages, we have both encapsulated and abstracted that information at the same time. Other objects can refer to the information by a message name, without knowing how that information is actually stored ± or even where.  This model generalizes naturally to messaging over a network, and to parallel computations.

By viewing computation as the interaction of numerous objects, each sending messages to other related objects, the object-oriented model provides powerful metaphors for recasting the tasks of any program via natural and intuitive analogies with the objects that inhabit the real world.  For example, the NeXT MusicKit is inhabited by familiar objects like Notes, Scores, Instruments, Orchestras, and even a Conductor.  Our innate understanding of these objects brings us in closer contact with the world being modeled, and thus with the computer representation of that world.

Objective-C arranges its classes into a tree hierarchy, in which every class inherits the properties of its parent, or superclass, and is then free to make changes to the parent's behaviour. For example, the NeXTstep Panel class is a subclass of the Window class. Panels are like windows except they are removed from the screen when the user clicks the mouse on another application, and they reappear on the screen when the user clicks the mouse on the Panel's application. The Panel class is itself subclassed to make the NeXTstep Menu class. Menus are like Panels, except they always float on top of all other windows, contain a set of menu cells, and can be made submenus of other menus.  Inheritance can contribute to all four goals.

Thinking like an object

Objects in the world around us are defined by their interactions ± pull a doorknob and the door opens; drop a glass and it breaks. This simple model also underlies all object-oriented languages. If an application's goals represent tasks in the real world ± like simulating an airplane, for example ± then they can be naturally implemented in an object-oriented way. One class of object could represent the landing gear, another the fuel tank, and so on. When the simulation runs, the objects all interact by sending messages to each other, and the virtual airplane flies.

Many traditional computer applications can be thought of in the same way.

Within any NeXTstep application, for example, each object that is visible on the screen ± each object you can touch with the mouse ± has an underlying object associated with it that ªfeelsº your touch. When a Button object is pressed, a message called mouseDown: is sent to the button, which then sends another message to its target object to carry out the requested action. It is the interaction of these active objects ± each knowing how to display itself, modify itself, and interact with the other objects on the screen ± that creates the whole NeXT environment.

Ergo, a game

Ergo is an implementation of a simple board game from a video arcade. The program is simple enough to be implemented quickly yet clearly shows many of the characteristics of good object-oriented design. 

Ergo is a little like Go, Othello, and Reversi. Players take turns moving a piece of their color either one or two squares. A one-square move replicates the old piece, creating a new piece in the new square. Two-square moves are jumps; the moving piece does not stay behind, but travels into the new square. A piece can't land on a square that is blocked or otherwise occupied (it can, however, jump over such squares). If, in its new position, the moved piece is directly adjacent to pieces of the opposite color, it captures those pieces by switching them to its color (see Figure 1).

Whenever you design a new NeXT application, always start by designing the most important part: the user interface. Often, you will have a good idea of what behaviour you want for your users but an unclear idea of the internal details. In addition, Interface Builder is an excellent sandbox for trying out ideas and throwing out the bad ones without wasting much effort. Interface Builder will also create and organize all the files you'll need to build and maintain your application over time, so starting there makes sense.

In this case, the interface is dictated by the game we are writing. There must be a game board (a Window) and a whole array of squares, some filled and others not (see Figure 2). To move, the user clicks the mouse in a filled square, drags it to an empty square and releases the mouse, changing an empty square into a filled one. Each square should ªfeelº the mouse and know whether or not it is filled and whether it can be legally part of the current move. (Notice that we have already begun to somewhat personify the squares, thinking of them as separate, active participants in the game. This is a valuable way to think about the communication between the parts of most object-oriented programs.)

The next step is to think about the various classes of objects we will have in the game; finding natural classes is the most important step in the design effort. If you feel uncomfortable with the choices that you've made, scrap them and start over. It's always better to begin anew than to stick with classes that don't naturally fit your problem.

Since we want to represent the whole board, and some squares on the board have no pieces, we probably want our most abstract ªpiece objectsº to be the squares of the board rather than the pieces themselves. This allows us to have both empty and filled squares. Some squares cannot contain pieces ± they're blocked ± and they form a third class of square. Finally, filled squares can contain either black or white pieces. The resulting class hierarchy is shown in Figure 3.

Notice that to represent the squares of the game board, we've created a new class: AbstractSquare, from which all other square classes will inherit characteristics. AbstractSquare is a subclass of the NeXT AppKit class called View. As a View subclass, objects of the class AbstractSquare (or any of its subclasses) will automatically be able to receive mouse events and draw themselves in windows. 

Once you have a hierarchy of classes, the next step in designing any application is to flesh it out with a protocol of key messages that the objects will use for communication and to drive the action of the application. Look at the protocol for Ergo in Figure 3. From the density of messages, you can see that the design focuses most of the behaviour in the Abstract classes, some in ErgoApp (our subclass of the AppKit's Application object), and very little in the other peripheral classes, which will mostly inherit their behaviour from their superclasses. Cooperation between objects from these various classes will drive the game forward.

Mouse behaviour is mostly handled by the class AbstractFilledSquare, which we would expect since game moves involve manipulating filled squares. Note that the processes of capturing, checking adjacent squares, taking turns, and distinguishing between black and white squares are all suggested even within the simple protocol of Figure 3.

Finally, we must flesh out the details of the design by implementing it. Learning the tricks and idioms of object-oriented implementation is a matter of experience, but having a natural hierarchy of classes and a sense of freedom about the patterns of message-passing that you will allow between your objects takes you far along the road. In examining the parts of the implementation presented below, you can read between the lines to get a feeling for how the design choices were made. The results will often suggest the approaches underlying them.

Details, details

When any NeXTstep application begins, the program's Application object (or in this case, the program's ErgoApp object) automatically gets sent the appDidInit: message (see Listing 1). ErgoApp's appDidInit: method sets who goes first (white) and asks all of the squares on the board to initialize themselves by sending each square the same message. This distribution of a function across a whole set of objects is a common theme in object-oriented programming, and we knew we would do it often enough to provide a general message for it: makeSquaresPerform:.

The method for makeSquaresPerform: works because every square on the board ± every View ± is really a subview of the master View object that displays the board's window, called the contentView. Every View keeps a List of all of its subviews; to get that List, we simply send that View the subviews message. That's what the method for viewList in ErgoApp does. Since all the Views we have in the window are squares of one type or another, this will forward the message to all the squares on the board.

In general, the squares in the Ergo game communicate by broadcasting their messages to every square on the playing board; the only squares that actually do anything are those which can help advance the task at hand. For example, most squares will respond to the broadcast message appDidInit by setting a tracking rectangle for themselves (Listing 2), but BlockedSquares (Listing 4) will do nothing. 

Setting a tracking rectangle is the way to tell the WindowServer that you want to be told when the mouse enters and exits a particular region on the screen. Tracking rectangles let a View receive mouseEntered: and mouseExited: messages in addition to the usual mouseDown: and mouseUp: messages received as the user moves and presses the mouse in the View. Since BlockedSquares never wants to receive such messages (it doesn't matter if you move the mouse into a BlockedSquare), they simply override the default appDidInit to not set a tracking rectangle. 

Once the tracking rectangles have been set, the rest of the game is entirely event-driven: Only when the user moves into a View or presses the mouse will anything further happen.

Make your move

To understand the handling of a move, you should know that each square can potentially be highlighted, and that each filled square can also be selected. Highlighting means that a square can participate in the current move: A square automatically highlights when you move the mouse into it while you are thinking about making a valid move. Highlighting is shown visually as a band of gray just inside the square's border. Selection indicates that a square contains the piece that is trying to move during this turn. It is shown visually as a large gray ªXº in the center of the square. 

As the user moves the mouse, all filled squares of the proper color will highlight as they are passed over. AbstractFilledSquare's method for mouseEntered: implements this behaviour (Listing 3). As long as either the square is selected, or it is the proper color and no one is selected, the square will highlight itself. The actual highlighting is done by AbstractSquare's method for setIsHighlighted: (Listing 2), which sets the proper state, records it globally if necessary, and then redisplays the square (self) to show the new gray band (see the method for drawSelf:). This globally recorded state, handled by our application object (NXApp), is one of only two or three small bits of state information we were unable to distribute across the squares. Listing 2 also contains the default mouseExited: implementation, which, if highlighted, unhighlights the square. Placing this here allows the two subclasses below that use mouse movement to share this one implementation.

When the user pushes down on the mouse, any non-filled square will ignore it (since they don't implement a mouseDown: method). But properly colored filled squares will handle a mouseDown: message (Listing 3) by selecting themselves. When the setIsSelected: message is sent, another piece of globally recorded state is changed. This global state is the key to what happens next: When the mouse now moves about, filled squares will again receive mouseEntered: messages, but this time isSomeoneSelected (Listing 1) will be true, so only the originally selected filled square will highlight (allowing a player to drop the piece back on the original square).

Now we would like all legal empty squares to highlight themselves so the user can see the legal squares to move to. For this, the other implementation of mouseEntered:, in EmptySquare (Listing 4), becomes important. In that method, the empty square asks all squares to check their adjacency with itself. If one of them finds that it is adjacent and has a legal move to the empty square sending the message, it will send that empty square back the message foundAdjacent (just below in Listing 4). This, in turn, causes that empty square (where we started) to highlight itself. 

To see how checkAdjancenyWith: works, we'll begin in AbstractSquare (Listing 2). Most squares simple ignore the message. However, filled squares (Listing 3) override that version to ask themselves, ªAm I selected and am I two squares away (or less) from the sender?º An affirmative answer means that the filled square has a legal move to the sending empty square, and it tells the empty square so. The empty square may get many affirmative answers, but this won't matter.

The user, who is still holding down and moving the mouse, can now see all legal squares where the selected piece can move. Eventually the user gets tired and releases the mouse, which always sends a mouseUp: message to the same object that received the mouseDown:. This has to be a filled square. Thus, the rest of the rules of the game are implemented in the method for mouseUp: (Listing 3).

Mouse up!

When the mouse is released, the square that the user wanted to move into should still be highlighted. If that square is the same square that received the mouseUp: message, we do nothing ± this is not a move, since the user pressed and released the mouse in the same square.

Otherwise, if the EmptySquare is still highlighted, we can perform a legal move. First, we remember the class of the highlighted square and how far we are away from it. Then we ask all the squares to perform a capture: with the highlighted square (an EmptySquare), after we have made it become: the same class that we are (this makes it a filled square of our color). We'll explore capturing in a moment. If we have jumped from more than one square away, we must vacate our old location (we become: the class of the empty square we just moved to, i.e., we become empty). Otherwise, we need only deselect ourselves and stay where we are (we have replicated ourselves). Finally, we ask the ErgoApp object to letOtherColorMove.

The last two details of a move are capturing and taking turns. Most squares do nothing for a capture: (see Listing 2). In AbstractFilledSquare (Listing 3), though, a filled square tests whether it should be captured by the sender, a matter of being a different color (class) and of being exactly one square away. If it should be captured, it will become: the same class as the sender (change to the sender's color). Many filled squares can be captured by the same sender in this way. It is a rather simple and elegant way to express the capture rule.

Turn-taking is handled by ErgoApp (Listing 1) via the message letOtherColorMove. The implementation inverts the answer that isWhiteMove will return and simply redisplays the board. Turn-taking alternates forever, since this version of the method does not check for the end of the game (see the sidebar on Extending Ergo).

On becoming

When programming in an object-oriented language, situations frequently arise in which one object wants to literally become another object. Often the objects will simply switch identities. Our become: (Listing 2) has an interesting implementation. Since we want to preserve the ªidentityº of the square, we record and pass along all the state information associated with the square, but assign them to a square of an entirely different class. In an unusual move, the original object then frees itself (by sending itself the free message) and returns the newly created object in its place.

The final point of some interest is the implementation of the subclasses BlackFilledSquare and WhiteFilledSquare. Rather than having an instance-dependent variable hold the black/white distinction, we use the class hierarchy to store the information directly.  For example, in class WhiteFilledSquare, the  color message returns NX_WHITE explicitly; in BlackFilledSquare the same message returns NX_BLACK.  One nice side-effect: Any new classes created later can choose to return a shade of gray without changing the rest of Ergo.

Hiding an object's state behind messages may seem frivolous at first, but there is power in the abstraction: We can choose to implement color's response as a state-variable lookup, a computed value, or as a constant (as it is here). Any of these implementations would behave identically under the protocol of messages we have defined; if the way color is implemented changes, nothing would have to be changed in the other objects that use the color message. Flexibility like this, which enhances the reusability of a class, begins to demonstrate the important benefits object orientation brings to application development, maintenance, and reuse.

The winning way

Whenever you design an application, start by asking yourself how the tasks could best be broken up and distributed to a whole host of small, cooperating objects, each of which does one thing well. 

Whenever you set out to design an object for your application, ask yourself what other purposes it could be used for, how best it could be extended to more fully generalize the one function needed today, and what implementation will be clean and clear enough that you will be able to reuse it in the future. 

The simple classes in Ergo have each tried to live up to these standards. As a bonus, you may discover that your application itself is now general enough to be reused by your users within the larger NeXT environment. Good object-oriented design can be applied to all levels.

Object-oriented thinking can help you achieve simple, natural, and elegant solutions. The old 90/10 rule should tell you that most of your program can afford the abstraction and clarity that a high-level, object-oriented design will bring. Don't settle for the familiar or traditional; that leads to programs that are unique, fragile, and difficult to share. Use object-oriented programming for your next project, and begin to build for the future.