Structure of Object-Oriented systems
Objects are state machines
Real-life entities have state. Your checking account has an amount of money; your car has a mileage, a gas level, a speed and so on. Objects in an application should also have a state and a set of operations that can manipulate that state. (You can change the color of your car or add money to your checking account; you cannot do the opposite.)
That being said, we normally want to hide the state and expose operations because it reduces coupling, which in turn reduces the "brittleness" of the whole system.
Tell, don't ask
Objects shouldn't have getters and setters; this is sometimes called the "tell, don't ask" principle. There are two cases where getters appear to be necessary:
- Asking an object for some information and then deciding what how to change the state of the same object. This code could be a fragment of a ping-pong game:
- if (ball.Y < 0 || ball.Y > MAX_Y)
- ball.SpeedY = ball.SpeedY * -1;
A much better way to do this is:
- ball.CheckYBounds();
thus encapsulating logic having to do with the ball inside the ball object.
- What about when you ask an object for its status and then modify *another* object depending on that status?
- if (ball.Y < 0 || ball.Y > MAX_Y)
- {
- ball.SpeedY = ball.SpeedY * -1;
- sound.Bell();
- }
Hmm. Changing this to having the ball object invoke
sound.Bell();
doesn't seem that good now. It creates a dependency between the ball object and the sound object. Even worse, what about drawing the ball on the screen? Allen Holub suggests having a method ball.DrawOnScreen();
- but this seems way too weird. The ball object is a "model world" object. It should only "know" about model world stuff - moving around, having coordinates, a size and a speed, interacting with other object. Having it know about drawing stuff seems dangerous. What if I want to save its state to a database? Should it know about that too? This is definitely a violation of the Single Responsibility principle (aka SRP) - an object should only have one reason to change.Communication between objects
One thing I read in the Turbo Pascal 6 manuals was this: every time you need object A to communicate with object B, think about adding a new object X in between them. At first, it sounds weird. However, if you need to add objects C, D and E to the mix, and all of them need to communicate with each other, then it pays for all of them to go through X: this way, you only need to have "send to X" and "receive from X" algorithms, instead of all the combinations. The clipboard is a good example of this: Word doesn't need to know how to communicate with Excel, Notepad, and a zillion other programs; it only needs to know how to put stuff into the clipboard and get stuff out of it.
So - how can this solve the above problem? How can the ball object inform the sound object that it needs to sound the bell, without creating a dependency between the two?
My solution would be a messaging system. We can have the ball object invoke the messaging object, which in turn invokes the sound object.
- void Ball.CheckYBounds()
- {
- //...
- if (ball.Y < 0 || ball.Y > MAX_Y)
- {
- ball.SpeedY = ball.SpeedY * -1;
- Messaging.SendMessage(SOUND_BELL);
- }
- //...
- }
- void Sound.OnBell() // answers to SOUND_BELL
- {
- //...
- }
Ok... the ball object is now free of the dependency on the sound object... it doesn't even have to know about the Sound class. However, it still has to know about the SOUND_BELL message. Why would a Ball know about making sounds? What if I want to reuse the Ball class in another application, where it's not supposed to make sounds when it gets reflected by the up and down edges? What if it's also supposed to flash when this happens - should I change the Ball class when I need to make a change to the way the ball is displayed? We're back to violating the single responsibility principle.
Inform, don't direct
Here is a great way of removing all such dependencies: don't have the object decide what other objects should do; just inform them of the state change and let the interested ones react to it (or not):
- void Ball.CheckYBounds()
- {
- //...
- if (ball.Y < 0 || ball.Y > MAX_Y)
- {
- ball.SpeedY = ball.SpeedY * -1;
- Messaging.SendMessage(BALL_HIT_HORIZONTAL_EDGE);
- }
- //...
- }
This has a few beneficial effects:
- The message sent by the ball is defined in the Ball class itself, where it makes sense. It made sense for the SOUND_BELL message to be defined in the Sound class, but in that case we still would have had a dependency from Ball to Sound, and the Ball class did not care about sounds. Any object that wants to react to the BALL_HIT_HORIZONTAL_EDGE message shouldn't have a problem about having a dependency on the Ball class - after all, it obviously cares about the Ball class. If we don't want the Sound class itself to have this dependency on Ball, that's fine: we can have a BallSounds object with dependencies on both Ball and Sound:
- void Ball.CheckYBounds()
- {
- //...
- Messaging.SendMessage(Ball.BALL_HIT_HORIZONTAL_EDGE);
- //...
- }
- void Sound.OnBell() // answers to Sound.BELL
- {
- //... ring a bell
- }
- void BallSounds.OnBallHitHorizontalEdge() // answers to Ball.BALL_HIT_HORIZONTAL_EDGE
- {
- Messaging.SendMessage(Sound.BELL);
- }
Note that both the compile-time and the run-time dependencies are in the directions we want: the Ball and the Sounds objects do not know about each other, and they also do not know about the BallSounds object. All the objects know only about the messaging system.
- A second advantage is that we've created an actual plug-and-play architecture, which has been a long-held dream of programmers, as far as I know. Let's say we decided to log one or more (or all) messages flowing through the system for debugging purposes. Simply create a new object, subscribe it to all the relevant messages (maybe add an all flag to the messaging system for this purpose) and have it log them when invoked. None of the other objects need to be modified in any way for this.
This is the architecture I'm envisioning:
The important point to keep in mind is this: an object should not send messages telling other objects what to do; it should inform them on what has been done and let them react to that.
to be continued...
Comments