Role-playing

The sniper lies in the bushes poised with his finger resting calmly on the trigger. He scans the environment, analyzing his enemy’s movement. He peers through the scope, marks his target, exhales, and pulls the trigger. Paint splatters the victim’s chest, and the referee signals to him that he’s been struck out.

A paintball game typically consists of two teams of players, and each player is assigned a role that they must fulfill, such as a point man or a rover. [1] In our example, the player assumes the role of a sniper. As he observes his environment, he binds himself to all sorts of messages ranging from understanding the battlefield situation to eliminating enemy players. The players themselves aren’t of much interest to the sniper. Rather, the roles they play are what matters. Questions he might ask himself are, “Is the player a friend or foe?” or “How should I determine sniping priorities?”

Geographic elements also play important roles, because players aren’t limited to people. The sniper needs to locate trees, bushes, bunkers, and other potential sniping spots that he can use to his advantage. While trees and bushes may seem like concrete details, they are actually roles that the sniper identifies as potential entities that can also play the role of sniping spots. The implication drawn here is that players might actually be roles themselves, and this does not apply only to paintball. Everything is a role-player, up until hard implementation is finally met. [2] This brings us back to messaging within the domain of computing.

Messaging is the key to object-orientation, and objects only exist to receive them. Many believe that objects are the abstractions in a system, but this is only the case because of the way de facto OO focuses on class-based programming instead of messaging. The abstractions really lie in the messages, more specifically, in the sets of messages that are sent by objects to other objects. A set of messages is an interface that defines a consistent set of behaviors, [3] so the focus of abstractions is based on interfaces. After all, the point of interfaces is to expose a coherent set of goals in a particular domain without revealing underlying representations.

By this point, it seems like objects are being neglected, but this isn’t the case. The question we should be asking is, “What are objects?” If a message is sent to invoke some kind of behavior to accomplish a goal, there must be some sort of process involved because a goal cannot be accomplished without an algorithm. Therefore, objects are really just processes invoked by message sends. Whether we realize it or not, we are always surrounded by objects. A book is a common example of an object, but objects don’t need to be concrete because like I said, objects embody processes. Gravity, society, and textual format are some abstract examples of objects, among infinitely many.

This leads us into role-playing. All systems require role-players, and it doesn’t matter who or what plays those roles, so long as they are fulfilled. In a movie, actors play the roles of different characters. A wolf can play the role of an alpha male. Peter Griffin can play the role of a gravitational field. Roles simply establish an expected interface that players within a domain must conform to, and the players are the objects that handle processes leading to different goals.

As mentioned earlier, a role can be played by any player. However, this doesn’t mean anything and everything should necessarily play a role. They should only do so if they are capable of doing so within their level of abstraction. That is, they have the necessary data and processes in place to play a role. Furthermore, they should play a role without exposing their data. After all, a role is an interface that’s used without concern for underlying implementation. This allows for reuse of players in multiple contexts because these contexts only care about the roles they need, not the players underneath.

For example, say we are creating a simple life game where the main character is initially loaded via the web, and plays the role of a doctor and a cook. Assuming we have already made a web request that returned a JSON payload representing the main character, we can start off with the following:

struct MainCharacterJSONPayload {
  private let jsonPayload: JSONPayload

  init(jsonPayload: JSONPayload) {
    self.jsonPayload = jsonPayload
  }

  // Behavior to access values
  // associated with the main
  // character
}

Here, I’ve created a struct that plays the role of a MainCharacterJSONPayload. It knows about the process required to extract main-character-related values from the JSON payload. Next, we have:

struct MainCharacter {
  private let jsonPayload: MainCharacterJSONPayload

  init(jsonPayload: MainCharacterJSONPayload) {
    self.jsonPayload = jsonPayload
  }
}

This is the MainCharacter role, which is initialized with the JSON payload. It understands high-level processes related to the MainCharacter role or other roles it might play.

Up until now, everything seems fine, but let’s say we want to load the character from a file for testing purposes because backend work is still incomplete and can potentially block further development of the game. We can introduce the following:

struct MainCharacterFile {
  // Behavior to access values
  // within the file that are
  // associated with the main
  // character
}

Now we have two ways of of representing a character – a JSON payload and a file. How should we structure MainCharacter? The inefficient solution would be:

struct MainCharacter {
  private var jsonPayload: MainCharacterJSONPayload?
  private var file: MainCharacterFile?

  init(jsonPayload: MainCharacterJSONPayload?) {
    self.jsonPayload = jsonPayload
  }

  init(file: MainCharacterFile?) {
    self.file = file
  }
}

This forces MainCharacter to check whether the JSON payload or the file has data before proceeding. What MainCharacter really wants is something that can play the role of a data source pertaining to the main character. Therefore, we should introduce a protocol that can be passed into MainCharacter:

protocol MainCharacterDataSource {
  // Behavior to access values
  // associated with the main
  // character
}
struct MainCharacter {
  private let dataSource: MainCharacterDataSource

  init(dataSource: MainCharacterDataSource) {
    self.dataSource = dataSource
  }
}

Now MainCharacterJSONPayload and MainCharacterFile can simply conform to MainCharacterDataSource before they’re used in MainCharacter.

struct MainCharacterJSONPayload, MainCharacterDataSource {
  private let jsonPayload: JSONPayload

  init(jsonPayload: JSONPayload) {
    self.jsonPayload = jsonPayload
  }

  // MainCharacterDataSource
  //
  // Behavior to access values
  // from the JSON payload that
  // are associated with the
  // main character
}
struct MainCharacterFile, MainCharacterDataSource {
  // MainCharacterDataSource
  //
  // Behavior to access values
  // within the file that are
  // associated with the main
  // character
}
MainCharacter(dataSource: mainCharacterJSONPayload)
MainCharacter(dataSource: mainCharacterFile)

Now that the data source is taken care of, the main character can now focus on the roles it needs to play. We’ll start with the doctor.

protocol Doctor {
  func diagnose(_ patient: Patient)
}
struct MainCharacter, Doctor {
  private let dataSource: MainCharacterDataSource

  init(dataSource: MainCharacterDataSource) {
    self.dataSource = dataSource
  }

  // Doctor
  func diagnose(_ patient: Patient) {
    // Diagnose the patient
    // while making use of
    // data source. Use your
    // imagination. Maybe the
    // algorithm makes use of
    // different skill levels
    // and personality types.
  }
}

And now the cook.

protocol Cook {
  func cook(_ dish: Dish)
}
struct MainCharacter, Cook, Doctor {
  private let dataSource: MainCharacterDataSource

  init(dataSource: MainCharacterDataSource) {
    self.dataSource = dataSource
  }

  // Cook
  func cook(_ dish: Dish) {
    // Cook a dish while
    // making use of
    // data source. Use your
    // imagination. Maybe the
    // algorithm makes use of
    // different skill levels
    // and sleepiness level.
  }

  // Doctor
  func diagnose(_ patient: Patient) {
    // Diagnose the patient
    // in some way
  }
}

Now that the main character fulfills the roles of both a cook and a doctor, it can be passed into any context that requires those roles. Mission accomplished.

I think this example was pretty straightforward. The problem was easy to understand, and the proper roles were identified. However, there are problems where roles aren’t immediately apparent.

One such example comes from Yegor’s post on utility classes, which I find contains a couple good examples of role-playing. He has an example where a text file plays the role of a collection of strings, and another example where a Max class plays the role of a number (assuming Number is some kind of an interface). If you look at the comment section of that post, you’ll find that a lot of people look upon these examples with disdain.

One commenter writes, “Max is a number? – A maximum is only defined relative to a set of numbers. So it is a function that can be applied to a set of numbers and yields a number, but it is not a number itself.”

Another writes, “So you mean ‘max’ is not a function, not an operation upon numbers but is a ‘number’?”

There are many comments like these, which isn’t surprising. The two things that have been forgotten in that comment thread are messaging and the idea that objects are really just processes invoked by message sends. An instance of Max is an object whose process involves the calculation of the highest number in a collection of numbers. [a] A context that requires a number, or perhaps even a set of numbers, only cares about the number role and doing number-related tasks. It needn’t know that it is actually dealing with a process that uses the highest number in a collection.

I think by now, it is apparent that understanding object-orientation requires a shift in perspective. OO was meant to move away from data-centric thinking, and yet, that kind of thinking still dominates many developers today. How much effort will it take to save others from drowning in a sea of strawmen? I hope that by using role-playing as one of many heuristics for designing systems, we can slowly pull ourselves out of the hole we’ve found ourselves in.

And please remember that I am learning as well, so I welcome any constructive feedback any of you might have.

Footnotes

[a] Yegor’s Max constructor takes in two numbers, but it can actually take in a collection of numbers

References

[1] Paintball positions

[2] Mark Miller: Are utility classes good oo design?

[3] Mark Miller: Object-oriented programming – In layman’s terms…

 

Leave a comment