Roles, Responsibilities, Collaborators
We try to think about objects in terms of roles, responsibilities, and collaborators, as best described by Wirfs-Brock and McKean.
- An object is an implementation of one or more roles;
- a role is a set of related responsibilities;
- and a responsibility is an obligation to perform a task or know information.
- A collaboration is an interaction of objects or roles (or both).
Sometimes we step away from the keyboard and use an informal design technique that Wirfs-Brock and McKean describe, called CRC cards (Candidates, Responsibilities, Collaborators). The idea is to use low-tech index cards to explore the potential object structure of an application, or a part of it. These index cards allow us to experiment with structure without getting stuck in detail or becoming too attached to an early solution.
Tell, Don’t Ask
We have objects sending each other messages, so what do they say? Our experience is that the calling object should describe what it wants in terms of the role that its neighbor plays, and let the called object decide how to make that happen.
This is commonly known as the “Tell, Don’t Ask” style or, more formally, the Law of Demeter. Objects make their decisions based only on the information they hold internally or that which came with the triggering message; they avoid navigating to other objects to make things happen. Followed consistently, this style produces more flexible code because it’s easy to swap objects that play the same role. The caller sees nothing of their internal structure or the structure of the rest of the system behind the role interface.
Designing for Maintainability
We grow our systems a slice of functionality at a time. As the code scales up, the only way we can continue to understand and maintain it is by structuring the functionality:
- into objects,
- objects into packages,
- packages into programs,
- and programs into systems.
We use two principal heuristics to guide this structuring:
Separation of concerns
When we have to change the behavior of a system, we want to change as little code as possible. If all the relevant changes are in one area of code, we don’t have to hunt around the system to get the job done. Because we cannot predict when we will have to change any particular part of the system, we gather together code that will change for the same reason. For example, code to unpack messages from an Internet standard protocol will not change for the same reasons as business code that interprets those messages, so we partition the two concepts into different packages.
Higher levels of abstraction
The only way for humans to deal with complexity is to avoid it, by working at higher levels of abstraction. We can get more done if we program by combining components of useful functionality rather than manipulating variables and control flow; that’s why most people order food from a menu in terms of dishes, rather than detail the recipes used to create them.
Encapsulation and Information Hiding
We want to be careful with the distinction between “encapsulation” and “information hiding.” The terms are often used interchangeably but actually refer to two separate, and largely orthogonal, qualities:
Ensures that the behavior of an object can only be affected through its API. It lets us control how much a change to one object will impact other parts of the system by ensuring that there are no unexpected dependencies between unrelated components.
Conceals how an object implements its functionality behind the abstraction of its API. It lets us work with higher abstractions by ignoring lower-level details that are unrelated to the task at hand.
We’re most aware of encapsulation when we haven’t got it. When working with badly encapsulated code, we spend too much time tracing what the potential effects of a change might be, looking at where objects are created, what common data they hold, and where their contents are referenced.
Object Peer Stereotypes
We have objects with single responsibilities, communicating with their peers through messages in clean APIs, but what do they say to each other?
We categorize an object’s peers (loosely) into three types of relationship. An object might have:
Services that the object requires from its peers so it can perform its responsibilities. The object cannot function without these services. It should not be possible to create the object without them. For example, a graphics package will need something like a screen or canvas to draw on—it doesn’t make sense without one.
Peers that need to be kept up to date with the object’s activity. The object will notify interested peers whenever it changes state or performs a significant action. Notifications are “fire and forget”; the object either knows nor cares which peers are listening. Notifications are so useful because they decouple objects from each other. For example, in a user interface system, a button component promises to notify any registered listeners when it’s clicked, but does not know what those listeners will do. Similarly, the listeners expect to be called but know nothing of the way the user interface dispatches its events.
Peers that adjust the object’s behavior to the wider needs of the system. This includes policy objects that make decisions on the object’s behalf (the Strategy pattern) and component parts of the object if it’s a composite. For example, a Swing JTable will ask a TableCellRenderer to draw a cell’s value, perhaps as RGB (Red, Green, Blue) values for a color. If we change the renderer, the table will change its presentation, now displaying the HSB (Hue, Saturation, Brightness) values.
Interface and Protocol
An interface describes whether two components will fit together, while a protocol describes whether they will work together.