grasp patterns best practices for object-oriented software design
TRANSCRIPT
GRASP Patterns
Best Practices for Object-Oriented Software Design
GRASP Patterns GRASP: Generalized Responsibility
Assignment Software Patterns GRASP patterns are really more accurately
described as best practices GRASP patterns outline best practices which can
be employed in any object-oriented design These best practices, if used properly, will lead
to maintainable, reusable, understandable, and easy to develop software
GRASP Patterns GRASP patterns describe how to assign
responsibilities to classes Warning: Grasps tend to be vague
Responsibilities is “a contract or obligation of a classifier” (UML definition) Responsibilities can include behaviour, data
storage, object creation and more They often fall into two categories:
Doing Knowing
1. Information Expert Assign a responsibility (such as behaviour)
to the information expert An information expert is the class that has (or
has direct access to) the information necessary to fulfill the responsibility
e.g. A responsibility such as handling a deposit (i.e. increase balance) should be assigned to the Account class This is because Account contains the account
balance as one of its attributes
1. Information Expert
This GRASP has the effect of having a class with high cohesion Cohesion – the degree to which the
information and responsibilities of a class are related to each other
Cohesion is improved since the information needed for a responsibility is closely related to the responsibility itself
2. Creator Give a class A the responsibility of creating
instances of another class, B, if: A is an aggregate of B A is a container of B
If no classes apply, then assign the responsibility to a class C if: C records instances of B C closely uses B objects C has the initializing data for B
2. Creator Whenever one class has the responsibility
of creating instances of another class, the two classes are coupled Coupling itself is not wrong, but we want to
eliminate certain types of coupling Coupling of classes is a measure of how
strongly a class is connected to another class Whenever two classes are coupled, one class
becomes dependent upon the other to function correctly
2. Creator
The Creator GRASP ensures that coupling due to object instantiation only occurs on closely related classes An aggregate or container of a class is
already coupled with that class Thus, assigning the creation
responsibility to the container or aggregate does not introduce more coupling
2. Creator e.g. Consider an Invoice which has a
number of InvoiceItems on it When a new Invoice is created, we might
wish to add new items to it It makes sense that the Invoice itself
would create instances of InvoiceItem, and subsequently add them to itself
The effect is that no other classes should need to know about InvoiceItems (at least not for this responsibility)
3. Low Coupling Assign a responsibility so that coupling
remains low This is pretty vague, but it means that we try to
keep low the number of classes to which a class is coupled
Creator is a more specific case of Low Coupling, related to instantiation
A good rule of thumb is: If class A is already coupled with class B, assign a responsibility for B to the class A This is only if it is not appropriate to assign the
responsibility directly to A (otherwise, this would be contrary to Information Expert)
3. Low Coupling The Low Coupling ‘pattern’ is definitely a
best practice It is a good idea to keep coupling low in a design
The reasons why Low Coupling is important should be obvious: With Low Coupling, changes to a class (A) affect
fewer classes (the classes coupled to A) Thus Low Coupling improves the maintainability
of a software system A low coupled class is also easy to understand,
since it is often simpler and more cohesive
3. Low Coupling e.g. Suppose the total price for an Invoice
needs to be calculated To achieve this, the costs must be totalled of all
InvoiceItem instances Recall that the Creator GRASP already
recommends that we create instances of InvoiceItem within Invoice Thus, Invoice is already coupled with InvoiceItem The responsibility of total price should be
assigned to Invoice, since coupling will not be increased
4. High Cohesion
Assign a responsibility so that cohesion remains high Again, this is vague, but it simply means
that we should always try to maintain class cohesion
This is another GRASP that is really more like a best practice, than a pattern
4. High Cohesion
When about to assign a responsibility, ask yourself the following question: Is this responsibility related to the
other responsibilities of this class? If not, there is likely a need to assign
the responsibility to another class This may prompt you to create a new
class if other responsibilities exist that are similar/related to this one
4. High Cohesion e.g. Consider a class Order, which
stores data about an order that has been placed in an online store Responsibility: Store the contents of an
Order to secondary storage Should this responsibility be added to
the Order class? Are order details and persistence
management related concepts? No!! They are not even close!
4. High Cohesion To preserve cohesion, Order should not be
given this responsibility So what class should persist order details?
Several possibilities exist, but here is one: A matching class (e.g. OrderDAO) whose
responsibility is merely to persist Orders Likely OrderDAO will have some persistence
code reuse (from a superclass or some other class or subsystem)
4. High Cohesion Cohesion is important because incohesive
classes are large and cumbersome Such classes are usually thousands of lines long
These classes are difficult to understand and maintain
It is also highly unlikely that a class with so many unrelated responsibilities will be useful in any other context There is little chance of class reuse
4. High Cohesion By contrast, cohesive classes represent a
single abstraction and responsibilities related to that abstraction Classes representing a single abstraction could
be reused in any context that requires modeling that abstraction
Class reuse is possible when highly cohesive Cohesive classes are easy to maintain
How do you find a bug related to storing order details to the database? OrderDAO
Cohesive classes are more modular, and thus support teamwork
Cohesion and Coupling Cohesion and Coupling are two of the most
important concepts in software design However, they are not completely
unrelated: Highly coupled classes often are not cohesive A class that has been assigned many
responsibilities for many external classes is unlikely to represent a single abstraction
5. Controller Assign the responsibility for receiving and
handling a system event message to a class that is either: Representative of the entire subsystem (e.g. a
Façade Controller) Representative of the entire use case scenario
Do not assign these responsibilities to View classes (windows, dialogs, etc.) A Controller is never a user interface object
5. Controller
This GRASP is just common sense: When a system event occurs, the class
who has the responsibility of performing a high level function should receive events indicating it should take place
Often a subsystem will have one or more Controller classes, each designed to handle certain responsibilities Controller responsibilities are usually
extremely high-level
5. Controller e.g. In a bank system, we might have a
Controller that manages all banking transactions (TransactionController) This class would have methods such as:
deposit() withdraw() payBill() transfer()
It makes sense that this class receive the event generated when the teller clicks the ‘Execute Bill Payment’ button on the user interface
5. Controller Some architectures stress the importance
of view/controller separation e.g. Some variations of the MVC, Layers
For these architectures, the Controller GRASP has to be slightly modified: One user interface might generate different
types of events than another UI To solve this problem, a View class can catch the
event, and generate a high-level event corresponding to the requested behaviour
The Controller would handle this event
5. Controller The GRASP mentioned that a Controller that
represents an entire subsystem might be called a Façade Controller This is a variation of Façade, where the Façade
receives events (as well as method invocation messages) and forwards them to the correct subsystem component for handling
This is essentially the same concept as the Façade described earlier in the course
5. Controller Warning: Watch out for bloated Controllers
This can happen if you have one Controller for the entire system, for example
Controllers can receive several events, but they should delegate the responsibility of the corresponding functionality to other classes e.g. The Façade Controller will receive the
event, and send a corresponding message to the correct subsystem component to handle it
One global Controller is usually a bad idea One common practice is to create a
Controller for each use case
5. Controller View classes generally should not receive
system events The reason for this is that when View classes
receive system events, they must call the corresponding method in another class to handle the behaviour
This couples the View with this other class We’ve already discussed why coupling should
be avoided (especially between modules) Worse yet, the View class actually handling the
behaviour would definitely reduce cohesion The View class would have at least two
purposes: user interface and behaviour
Coupling Coupling cannot be avoided altogether
Without coupling, all we could create would be isolated and static classes
Our software wouldn’t be able to do anything Reducing coupling should be one of the
factors taken into consideration when assigning responsibilities
Also, specific kinds of coupling should be avoided: Coupling between two classes internal to two
different modules would be a big mistake as the internal details of a module should be hidden
6. Polymorphism When related behaviours vary by type
(class), assign the responsibility polymorphically to the specialization classes This is basically the purpose of polymorphism, so
it is natural for software developers to understand
This is not much of a pattern, and yet another best practice
6. Polymorphism e.g. Consider a UML diagram drawing
program If shapes are responsible for drawing
themselves via the draw() method: Obviously, an Actor (stick figure) will draw itself
differently than a UseCase (ellipse) It might make sense in this case to have the
classes themselves handle the drawing by polymorphically overriding the draw() method
An advantage is that new entities (e.g. State) can be easily added without changing the core graphics code
6. Polymorphism Polymorphism can lead to highly cohesive objects Consider the example where some draw() method
were implemented similarly to this:If (entity.type = “UseCase”) then drawEllipse(…);Else if (entity.type = “Class”) then
drawRectangle(…);…End if This is not highly cohesive, since it combines
unrelated behaviours, and it also strongly couples this object with the shape it draws
7. Pure Fabrication To support high cohesion and low coupling,
where no appropriate class is present: invent one Even if the class does not represent a problem
domain concept This is a compromise that often has to be
made to preserve cohesion and low coupling Remember: the software is not designed to
simulate the domain, but operate in it The software does not always have to be
identical to the real world
7. Pure Fabrication Pure Fabrication is a compromise when
faced with a choice between modeling the domain and preserving maintainability and class reusability Software maintainability and reuse are always
more important in business, since they are ways companies can preserve resources
Usually, Pure Fabrication is used when there is no appropriate class to use Usually, it is a good idea to try the other patterns
first to try to find a solution which more closely resembles the domain entities
7. Pure Fabrication e.g. In the previous example is was
suggested that entities in a UML editor draw themselves However, what if this is a difficult task? What if the drawing facilities vary depending on
what drawing facilities the user has installed? e.g. DirectX, OpenGL, etc.
The real world suggests that the Architect class draw the entities This is because architects draw UML diagrams
7. Pure Fabrication Architect might be used to store user
preferences, recently created diagrams and projects, etc. This would obviously be a cohesion problem
Pure Fabrication would suggest creating a class whose job is to draw the entities One solution might be to have the Entity
instances associate with an instance of EntityRenderer component
There could be a subclass of EntityRenderer for each subclass of Entity e.g. UseCaseRenderer, ActorRenderer
7. Pure Fabrication
The difficulty then becomes: how do we associate the entity instances with the right instance of renderer? This is a problem easily solved by the
AbstractFactory pattern, discussed later
8. Indirection To avoid direct coupling between objects,
assign an intermediate object as a mediator1. Recall that coupling between two classes of
different subsystems can introduce maintenance problems
2. Another possibility is that two classes would be otherwise reusable (in other contexts) except that one has to know of the other Coupling the two objects would reduce the
reuse contexts to where both abstract concepts were relevant together The objects could not be reused separately
8. Indirection e.g. Consider an application for managing
group work Employee instances might need to be coupled to
Project instances However, potential for reuse of both Employee
and Project is high One solution is to assign a class
(Assignment) to couple the two classes In this case, the class represents an association
class (a class that represents an association)
8. Indirection The Façade pattern is another example of
Indirection The Façade prevents coupling classes in two
different subsystems Classes in one subsystem communicate
directly with the Façade The Façade communicates with the correct
subsystem component Thus, changes to the structure of the
subsystem will not affect the user of the subsystem, thanks to the Façade
9. Protected Variations Assign responsibility to create a stable
interface around an unstable or predictably variable subsystem or component If a component changes frequently, the users of
the component will also have to be modified This is especially time consuming if the
component has many users Wrapping the component in a stable
interface means that when variations occur, the wrapper class need only be changed In other words, changes are localized
9. Protected Variations e.g. Big video game companies make
money by creating a 3D graphics game engine (as well as sound, AI, etc.) These video game companies often produce
many games using the same engine, and release the game on many consoles
This is only possible to this extent using Protected Variations If a game is to be ported to another console, the
wrapper object will have to delegate 3D graphics drawing to different console-level commands
However, the wrapper is simpler to change than the entire game and all of its facets
9. Protected Variations These video game companies facilitate
additional revenue by making their wrapper modules flexible, so that they can create many games using the same interface Thus, a good practice is to leave out
unnecessary details in wrappers It is often useful to try to preserve the cohesion
(and thus reuse factor) of a subsystem by keeping the interface flexible
Obviously, the Façade pattern is also an example of Protected Variations
9. Protected Variations One popular pattern using Protected
Variations is the Adapter An Adapter is another pattern, discussed later,
which adapts one interface for another Perhaps an application (e.g. a game) is written
to use a one interface (e.g. OpenGL for 3D graphics)
The application (game) might need to be adapted to also run on other platforms (e.g. DirectX/Direct3D)
An Adapter is intended for this kind of application An OpenGL-to-Direct3D adapter is possible
9. Protected Variations Among other examples are JDBC and ODBC:
These are packages that allow applications to access databases in a DB-independent way In spite of the fact that databases all use slightly
different methods of communication It is possible due to an implementation of
Protected Variations Users write code to use a generic interface
An adapter converts the generic method calls to DB-specific communications and vice versa
10. Don’t Talk to Strangers
Do not couple two objects who have no obvious need to communicate This is common sense: do not add
coupling where unnecessary Again, this is a best practice in
software Although common sense, this still must
be considered seriously, as a tendency exists to model all possible relationships
10. Don’t Talk to Strangers Often the domain objects have relationships
that need not be modeled in the application However, a common tendency is to model those
relationships (with aggregation, for instance) anyway
Model relationships only if they are necessary to complete a use case Ultimately it is not relationships in the domain
that determine if relationships in software should be present
10. Don’t Talk to Strangers e.g. Consider a convenience store
application Customers are people, as are Employees
Should we model this as inheritance? In this application, customers are likely
anonymous Thus, Employee instances (which likely store
names and phone numbers) do not share any common data or behaviour It makes sense that we not model this
relationship, despite our initial reactions