Looking for Common Ground
Earlier, we suggested that you make sharp distinctions between candidates. If you couldn’t find enough differences, we recommended that you merge candidates that have overlapping roles. Now we suggest that you take another, closer look at your candidates. This time you want to see what your candidates have in common. You should always be on the lookout for common roles and responsibilities that candidates share. If you can identify what candidates have in common, you can consciously make your design more consistent by recognizing these common aspects and making them evident. You can identify a common category that objects fit into. You can define a common role that all objects in a category play. Shared responsibilities can be defined and unified in interfaces. Objects that collaborate with them can ignore any differences and treat them alike. Furthermore, a class can be defined to implement shared responsibilities that make up a shared role, guaranteeing that the implementation of classes that inherit these implemented responsibilities works consistently.
You are likely to find several ways to organize your candidates. Some will be more meaningful than others. Each one that seems useful will likely contribute to your design’s clarity. The more you can identify what objects have in common, the more opportunities you have to make things consistent. Eventually you may define several new roles that describe commonly shared responsibilities. Your initial cut at this won’t be your last. Keep looking for what objects have in common and for ways to exploit commonalities to simplify your design.
Common behavior could also imply the need for another candidate that is the supplier of that shared behavior.
Look for powerful abstractions and common roles. Things in the real world do not directly translate to good software objects! Form candidates with an eye toward gaining some economy of expression. Carefully consider which abstractions belong in your object design.
In our Kriegspiel game, there are various actions that a player can perform: “propose a move,” “ask whether a pawn can capture in a move,” “suspend a game,” and so on. It’s a pretty safe bet that we have a different candidate for each action: ProposeAMove, SuspendAGame, and so on. Proposing a move seems quite distinct from suspending a game. A harder question is whether we should define PlayerAction as a common role shared by each of these action-oriented candidates. If we can write a good definition for PlayerAction, we should do so and define a role that is shared by all player action candidates. There seem to be several things common to all actions (such as who is making the request and how long it is active). Eventually, if we find enough common behavior for PlayerAction, it will be realized in our detailed design as a common interface supported by different kinds of PlayerAction objects. We may define a superclass that defines responsibilities common to specific player action subclasses. Or common behavior might imply the need for another candidate that is the supplier of that shared behavior.
Look for the right level of abstraction to include in your design. Finding the right level of abstraction for candidates takes practice and experimentation. You may have made too many distinctions and created too many candidates—a dull design that works but is tedious. At the end of the day, discard candidates that add no value, whether they are too abstract or too concrete. Having too many candidates with only very minor variations doesn’t make a good design. Identify candidates that potentially can be used in multiple scenarios.
Certain actions affect the position of pieces on a board. Should we have different candidates for each piece’s potential types of moves? Not likely. This solution is tedious and offers no design economy. If you can cover more ground with a more abstract representation of something, do so. A single candidate can always be configured to behave differently under different situations. Objects encapsulate information that they can use to decide how to behave. The Propose-AMove candidate can easily represent all moves suggested by any chess piece. This single candidate will know what piece is being moved and its proposed position.
Discard candidates if they can be replaced by a shared role. To find common ground, you need to let go of the little details that make objects different in order to find more powerful concepts that can simplify your design.
What do books, CDs, and calendars have in common? If you are a business selling these items over the Internet, they have a lot in common. Sure, they are different, too. Books likely belong to their own category of items that can be searched and browsed. But all these kind of things share much in common. They all have a description (both visual and text), a set of classifications or search categories they belong to, an author, an availability, a price, and a discounted price. It sounds as if their common aspects are more important, from the Web application’s perspective, than their differences. This suggests that all these different kinds of things could be represented by a single candidate, InventoryItem, that knows what kind of thing it is and the categories it belongs to.
Purely and simply, you gloss over minor differences. You don’t need to include different candidates for each category of thing. In fact, those distinctions may not be as important to your software as they are to those who buy and use the items.
When you are shopping for items, you may be thinking of how they are used—books are read, calendars hung on a wall, and CDs played—but those distinctions are not important if you are designing software to sell them. Sure, you want to allow for your software to recognize what category something belongs to. You want to list all books together. But you probably want to categorize things in the same subcategory, whether or not they are the same kind of thing. Books about jazz and jazz CDs are in the “jazz items” category.
Only if objects in different categories behave differently in your software do you need to keep different categories as distinct candidates. The real test of whether a category adds value to a design is whether it can define common responsibilities for things that belong to it.
Blur distinctions. There are times when both concrete candidates and their shared role add value to a design. There are times when they do not. If you clearly see that candidates that share a common role have significantly different behavior, then keep them. Test whether the distinctions you have made are really necessary.
What value is there in including different kinds of bank accounts, such as checking or savings accounts in our online banking application? Checking accounts, savings accounts, and money market accounts have different rates of interest, account numbering schemes, and daily account draw limits. But these distinctions aren’t important to our online banking application. We pass transactions to the banking software to handle and let them adjust account balances. In fact, because our application is designed to support different banks, each with its own account numbering scheme, a distinction made on account type (checking or savings) isn’t meaningful. Our application doesn’t calculate interest. So we choose to include only BankAccount as a candidate. If we were designing backend banking software that calculated interest, our decision would be different.