Warning: this is my own experience with Class-Responsibilities-Collaborators cards and you may have different opinions about how they should be used for object-oriented design, or have followed a different school of though altogether.
The goal of the cards (at least in how I have seen it in use today, and used myself) is to produce an abstract object-oriented design before diving into code. The coach that introduced them to me for real, Matteo Vaccari , says they give you an alternative to emergent design because you have to know both how to do upfront design and emergent design to consciously choose the latter.
It's like saying you have to know both the procedural and object-oriented style to consciously choose to write code in the former, and not just writing long procedures because it's the only way you know to structure code.
No judgment of up front and emergent design is intended in this article.
CRC cards are a product of the former, but can be used in many approaches; for example, the last time after not being satisfied with the tests for several objects, I turned to cards for modifying the design and experiment with solutions without having to move dozens of lines of code at the time.
What they are, and some definitions
Often object-oriented design is taught starting from principles that are actually implementation details, such as the conception of objects as data structures with functions inside them, or an overemphasis on inheritance. The thesis of CRC cards is that the actual cornerstones of OOP are classes, responsibilities, and collaborations between objects.
Since implementation details like a method are well defined: - a procedure attached to an object that accesses or manipulates its internal state and offers an higher level of abstraction. let's proceed to give some definitions for the other terms.
A class is the characterization of multiple objects: most definitions talks about a class as the blueprint for an object, but you can also think of a class as capturing the commonalities of all its instances. So it's possible to have a Animal class, but it's not said that we need the Cat and Dog subclasses to be considered if we can capture all the interesting behavior in the original Animal class.
A responsibility is something that an object knows, or can do. The sum of the responsibilities of all the objects will have to satisfy the specification of the system. This is the most fuzzy concept to capture: in Parnas's style, responsibilities can be thought as design decisions to hide, such as dealing with HTTP or a user interface, or hiding a database vendor inside a Repository.
An object B assumes the role of collaborator when is sent a message from another object A (in implementation terms, when it receives a method call).
We often talk of collaborators as the objects injected in A, but all of these are collaborators:
- objects injected in the constructor or via setters in A.
- Objects created internally in A, directly or through indirection.
- Objects passed inside messages (as method parameters).
- Objects globally available (if you decide to use them, and they are referred to in the current class).
The name of the class is written at the top, while during design we write responsibilities and collaborators on the rest of the card (on two columns, or on the top and bottom areas of the card; I don't think it really matters as long as there is available space. This paper format (3" x 5") is not very diffused in some countries, so I usually just cut A4 (Letter format) sheets into four parts.
The weight of the paper is not strong, but it's enough for the card to be moved around and manipulated for some Pomodoros.
Starting from objectsThe first way to use CRC cards is to define the objects and classes first. You look into the domain and write a card for each relevant concept, starting with names (User, Group, Vendor, Billing).
Not all of these objects will make it into the final design, as they may be discarded or absorbed into other objects as fields. Once you have several objects at your disposal, you can start simulate a use case or and end-to-end test by giving the control to one of them. The object will absolve a part of the use case (assuming a responsibility) and pass the control to one of its collaborators, which have to be decided now. This approach is similar to outside-in development, but there are no automated tests involved.
Starting from responsibilitiesThe second way to use CRC cards is to divide responsibilities into objects, whose names emerge from the domain or a metaphor after the fact. I think we use this style unconsciously while doing Test-Driven Design at the unit level: we write a specification for each object in the form of a test, and cut the problem into parts; we continue to refactor, in particular to rename, until we are satisfied with the design.
With cards, we can do the same, but we can experiment more solutions. You can redefine responsibilities and collaborations very quickly:
How they helpBoth approaches share the common trade-off of designing without code: the higher level of abstraction lets you experiment at a low cost (throwing away paper) different division of responsibilities and different conversations between objects.
However, the price you pay is that the translation of the design into code may put into light problems that were inexpressed at the higher level. There's no escape from having to explore requirements and be ready to retrieve the full specification for the system: if you don't know what your code should do, no design technique can save you.
Often inserting new responsibilities is easy, especially when you look at the organized cards. For example, recently I found out that I would have to check an hmac code for authentication, and it was evident which class should accomodate that responsibility. This visualization is similar to what I see when I look at Factory code, where an object graph is constructed.
There are differences, though: the cards capture some dynamic behavior like objects passed around, while the Factory only defines field references; the cards are also limited as a 2-dimension picture can be, and these arrangements can change depending on the use case under consideration, since they're not as fixed as an object graph.