ID:1811112
 
Welcome back yet again to another installment of Snippet Sundays. I'm still in a find mood, so you guys are getting a special Triple Sunday. Hooray!

In this installment, we're going to talk about the observer pattern. Some of you guys who know me know that I do an awful lot of talking about modular design. I've even covered this topic before! This is an important topic, though, and folks like Pixel Realms, the Spirit Age team, and the Space Station 13 community should be paying a lot of attention to.

If you work on a big project, the observer pattern is an absolute must for keeping it manageable. If you try to do things the hard-coded way, you'll actually find that your project performance and readability are going to suffer badly.

That said, I am not in any way claiming that this approach is faster or more memory efficient than the hard-coded method. In fact, it's not faster, and it's not more memory efficient. When comparing a hard-coded approach that uses the same logic as this approach, you'll find that the hard-coded approach will win in almost all cases.

Here's the rub, though: The observer pattern significantly reduces development time once you get used to it, and it allows you to completely remove unnecessary code from your functions. This reduces the likelihood of refactoring errors, increases code readability, and when used to construct a truly immense project, it will actually wind up being faster than the hard-coded method. This is because the observer pattern can bypass a significant amount of logic without having to use logic gates. Unneeded code simply doesn't operate. This would be impossible in the hard-coded variant. The observer pattern will, however, tend to eat up more memory. But you'll find that well-structured and well-designed code that doesn't use the observer pattern will use a very-similar amount of memory if it's put together properly. The negatives are minor. So minor, in fact that the positives of using the observer pattern shine through in many situations.


What is the observer pattern?

The short explanation is pretty simple. The observer pattern is a way make one or more objects interested in the state of another object. There are two main components in the observer pattern: The observer object, and the observable object. An object that is observable must be aware of what objects are observing it, and what specific state changes the observer is interested in. The observer object only needs to be aware of the behavior that it should trigger on a specified state change on the observable object. In many setups, an observer can actually be observing many different objects for different state changes.


What is a state change?

Well, a state change can best be described as "something happened". Anything you want can be a state change. It just depends on what you need observers to be able to do. For instance, a movable atom successfuly moving could be a state change. A movable atom failing to move could be a state change. A movable atom having its icon changed could be a state change. Hell, a movable atom not having any state changes for a specified period of time could be a state change. A state change is just a fancy way of saying: "We care about something that happened to this.".


What does the observer pattern do?

At a basic level, the observer pattern doesn't do anything. It allows you to make objects more aware of each other, and allows objects to more intelligently interact with one another based on what's going on elsewhere in your code.


What is it good for?

The observer pattern is good for situations where you don't exactly know what is going to happen when a state changes. Essentially, when you use the observer pattern, you are planning for the future. It allows you to be flexible in your program's design.

I often use observer patterns for dynamic behavior changes of objects. For instance, let's say I have an attack in a game that makes the player move half as fast for a few minutes. Most developers attack this like this:

mob
var
slowed
next_move
move_speed
Move()
if(next_move>world.time)
return 0
. = ..()
if(.)
if(slowed)
next_move = world.time + move_speed * 2
else
next_move = world.time + move_speed


This isn't bad, but it falls victim to a lack of ability to be customized. Let's see what happens when we add an ability that makes you move twice as fast:

mob
var
hasty
slowed
next_move
move_speed
Move()
if(next_move>world.time)
return 0
. = ..()
if(.)
var/spd = 1
if(slowed)
speed *= 2
if(hasty)
speed /= 2
next_move = world.time + move_speed*spd


Now the pattern has changed. We have to check two different variables. Our Movement code has become more complicated, and it's just become a tiny bit slower. You see, when the player has none of these status conditions that affect their movement, you wind up still having to check all of them, and that costs you a little bit of time every step.

What if there was a better way? What if there was a way you could only apply the status conditions that would affect the player when they were necessary? What if you didn't have to check whether these conditions were active at all? A perfectly healthy player would just move without doing all these checks. The observer pattern would make this possible.


A very basic example of the observer pattern:

effect
movement
proc
canMove(mob/mover,atom/NewLoc,Dir,step_x,step_y)
return 1

onMove(mob/mover,list/parameters)
return parameters

onMoved(mob/mover,atom/OldLoc,oldDir,ostep_x,ostep_y)

mob
var/tmp
list/move_listeners
proc
AddMoveListener(datum/d)
if(!move_listeners)
move_listeners = list(d)
else
move_listeners |= d

RemoveMoveListener(datum/d)
if(move_listeners)
move_listeners -= d
if(move_listeners.len==0)
move_listeners = null

canMove()
for(var/datum/d in move_listeners)
if(!call(d,"canMove")(arglist(args)))
return 0
return 1

onMove(list/params)
for(var/datum/d in move_listeners)
l = call(d,"onMove")(src,params)

onMoved()
for(var/datum/d in move_listeners)
call(d,"onMoved")(arglist(args))

Move(NewLoc,Dir,step_x,step_y)
if(canMove(src,NewLoc,Dir,step_x,step_y))
var/OldLoc = loc
var/oDir = dir
var/oSx = src.step_x
var/oSy = src.step_y
onMove(args)
. = ..()
if(.)
next_move = move_delay + world.time
onMoved(src,OldLoc,oDir,oSx,oSy)
return .
return 0


The above example code defines three events that all get grouped into a single list: move_listeners. These three events are: "canMove", "onMove", and "onMoved".

Adding new combat effects with this setup is trivial now! Let's see a few examples.

effect
movement
stunned
canMove(mob/mover,atom/NewLoc,Dir,step_x,step_y)
return 0
confused
onMove(mob/mover,list/parameters)
parameters[1] = mover.loc
parameters[2] = pick(NORTH,SOUTH,EAST,WEST,NORTHEAST,NORTHWEST,SOUTHEAST,SOUTHWEST)
if(parameters[2]&EAST)
parameters[3] = mover.step_x + mover.step_size
else if(parameters[2]&WEST)
parameters[3] = mover.step_x - mover.step_size
else
parameters[3] = mover.step_x
if(parameters[2]&NORTH)
parameters[4] = mover.step_y + mover.step_size
else if(parameters[2]&SOUTH)
parameters[4] = mover.step_y - mover.step_size
else
parameters[4] = mover.step_y
bleeding
onMoved(mob/mover,atom/OldLoc,oDir,ostep_x,ostep_y)
new/obj/blood_trail(OldLoc,oDir,ostep_x,ostep_y,mover)
mover.hp -= 1
//there are better ways to do the two below, but this is just for sake of example:
slowed
onMoved(mob/mover,atom/OldLoc,oDir,ostep_x,ostep_y)
mover.next_move = (mover.next_move - world.time)*2 + world.time
hastened
onMoved(mob/mover,atom/OldLoc,oDir,ostep_x,ostep_y)
mover.next_move = (mover.next_move - world.time)/2 + world.time


Now, that's all well and good, but I don't like repeating code all over the place. I've developed a really convenient event handler.

...To be continued later...
Any plans on continuing this? Interested in seeing what you do.
Full disclosure:

I'm not in very good shape right now. I'm recovering from a full HDD failure ATM (Never buying "green" products again), and I'm in the midst of a really bad depressive episode. I don't really want to publicly go into my problems in specific, but suffice it to say that I've been struggling with PTSD for the last four years and have had a few short term hospitalizations relating to suicide attempts in the last two years.

I don't know if I'll be in the kind of shape to carry on with much of anything for a little while. At least until I can find my way out of the hole I'm in at the moment.

I appreciate your interest in this series, and as always, appreciate the community's unwavering tolerance of my sometimes abrasive nature.
In response to Ter13
Ah, no problem.

Take care of yourself.