GRASP Design Patterns
Welcome to a comprehensive article that will explain design patterns from the GRASP group for you. These were composed by Craig Larman, a popular author and programmer interested in software design and the development process. GRASP is an acronym of General Responsibility Assignment Software Patterns. The question of assigning responsibility is a persistent problem in OOP applications and one of the most important pillars of a high-quality architecture. We've already talked about the importance of assigning responsibility in the Software Architecture and Depencency Injection course. Unlike, for example, the "GOF Design Patterns", GRASP doesn't provide specific implementation patterns, but rather best practices. For this reason, we'll be able to describe all the GRASP patterns in a single lesson today with no problems.
Controller
You, as programmers with an interest in software design, should know the term "controller" very well, at least in the context of the "MVC architecture". It works a bit like a remote TV controller or an Xbox controller. It's a component whose role is to communicate with the user. We can find variations of the controller pattern basically in all well-written applications. For example, C# .NET form apps use the Code Behind layer, which is a controller. When the communication with the user is mediated through a separate control class, the application gets divided into layers and its logic is fully encapsulated from the presentation. Such applications are readable and easy to maintain.
Let's show a simple example. Consider we're programming a calculator. A deterrent example of a monolithic application would look as follows (using the C# language):
public int Add() { Console.WriteLine("Enter the 1st number"); int a = int.Parse(Console.ReadLine()); Console.WriteLine("Enter the 2nd number"); int b = int.Parse(Console.ReadLine()); return a + b; }
In the method above, all the communication with the user (writing and reading
from the console) is mixed with the application logic (the calculation itself).
In practice, of course, the method would calculate something more complicated so
it'd be worth it to write it. Imagine that there's a much more complicated
operation instead of the a + b
. Sometimes, we say that such a
method has side effects, so it's not universal and calling it
also triggers some console communication which might not be noticeable at first
glance. This problem is pretty obvious here and perhaps it won't come to your
mind to write the application this way.
However, the same problem can be less noticeable when we write application logic directly to the event handler methods of form control elements. You've already programmed a form application for sure. You may have seen code like this:
public void AddButton_Click(Object sender) { int a = int.Parse(number1.Text); int b = int Parse(number2.Text); resultLabel.Text = (a + b).ToString(); }
We contaminate the controller, the control class, with application logic (the calculation). In all applications, there should always be one layer that only serves to communicate with the user, whether human or an API. This layer shouldn't be missing (the first wrong code) or shouldn't do anything else (the second wrong code).
The correct version of the console calculator code would be, for example, as follows:
public static function main() { Calculator calculator = new Calculator(); Console.WriteLine("Enter the 1st number"); int a = int.Parse(Console.ReadLine()); Console.WriteLine("Enter the 2nd number"); int b = int.Parse(Console.ReadLine()); Console.WriteLine (calculator.Add(a, b)); }
The main()
method is, in this case, a part of a controller that
only communicates with the user. All the application logic is encapsulated in
the logic layer classes, here in the Calculator
class. This class
doesn't contain any interactions with the console.
A correct version of the second example would look the same:
class CalculatorController { private Calculator calculator = new Calculator(); public void AddButton_Click(sender: Object) { int a = int.Parse(number1.Text); int b = int.Parse(number2.Text); resultLabel.Text = (calculator.Add(a, b)).ToString(); } }
And the UML diagram:
We can see that controller's responsibility includes parsing as well because
it's an input processing. The same is with changing the resultLabel
label value, which is the output. However, the calculation itself is in the
Calculator
class again, which doesn't even know about the form.
In order to provide universal examples, let's also show how rendering a page without a controller looks like in PHP:
<?php $database = new PDO('mysql:host=localhost;dbname = testdb;charset=utf8mb4', 'name', 'password'); $cars = $database->query("SELECT * FROM cars")->fetchAll(); ?> <table> <?php foreach ($cars as $car) : ?> <tr> <td><?= htmlspecialchars($car['license_plate'])?></td> <td><?= htmlspecialchars($car['color'])?></td> </tr> <?php endforeach ?> </table>
And with the controller:
class CarController { private $carManager; public function __construct() { $this->carManager = new CarManager(); } public function all() { $cars = $this->carManager->getCars(); // A template variable require('Templates/cars.phtml'); // Loads the template } }
The "car.phtml" template would look like this:
<table border = "1"> <?php foreach ($cars as $car) : ?> <tr> <td><?= htmlspecialchars($car['license_plate']) ?></td> <td><?= htmlspecialchars($car['color']) ?></td> </tr> <?php endforeach ?> </table>
We have separated the logic and the presentation into 2 files and reduced the coupling.
Creator
The Creator
pattern answers the question into which
class we should place code creating an instance of another class. Craig
says that the B
class instantiates the A
class if:
1. A is a part of it
An example could be Invoice
and InvoiceItem
classes. It makes sense to create instances of invoice items in the
Invoice
class as they're clearly part of it. The
Invoice
class has the responsibility for its items here.
2. A is its dependency
The B
class creates A
if it depends on it. An
example could be e.g. a signature database whose instance would be created by
the Invoice
class so that it can display a signature on invoices it
generates. If this dependency is also used somewhere else, it's better not to
create new instances of the dependency all over again, but to use the Dependency
Injection pattern.
3. Has enough information to instantiate it
Typically, there are multiple places where we could create a class instance. However, we should create it in a place where all information are already available, meaning the variables or instances needed for the creation. It doesn't make much sense to retrieve additional data into some class when everything is already available elsewhere.
As an example, let's decide whether to instantiate the
InvoiceList
class with customer invoices in the
InvoiceManager
or CustomerManager
class. Let's find
out which of the classes has all the information that InvoiceList
needs. If we need, for example, to get all the invoices and select those of a
particular customer, we'll instantiate InvoiceList
in
InvoiceManager
since the manager has all the invoices.
4. B contains A
If A
is a nested class in the B
class, its instance
should also be created by the B
class. However, nested classes have
not become very popular.
High cohesion
High cohesion means that our application consists of reasonably large pieces of code, each one focusing on one thing. This is also one of the basic principles of the OOP itself. High cohesion is closely related to low coupling (see below), because when we concentrate related code to one place, the need to reference other parts of the application is reduced. Another related pattern is the Law of Demeter, which basically says that objects shouldn't "talk" to foreigner objects.
An example of High Cohesion is, for example, concentrating the functionality
related to users into the UserManager
class. If we placed e.g. the
code logging the user in into the InvoiceManager
class, where it
would be required in order to display the user's invoices, or we placed the code
canceling the user's account into the Cleaner
class, removing
inactive accounts, we'd violate High Cohesion. The code that should be together
in the UserManager
class would be fragmented in different places of
the application, where it was just needed. Therefore, we concentrate the related
code to one place, even if these methods are used only once within the
application.
Indirection
Indirection is a very interesting principle which we've already encountered with the Controller. It says that when we create a "man in the middle" class somewhere in our application, i.e. an "extra" class, it can paradoxically simplify the application. We can clearly see that controllers reduce the number of references between objects, and at the cost of just a few lines of code, they also support better code re-usability and readability. Indirection is one of the ways to achieve Low coupling. We've already introduced an example with the Controller pattern.
Information expert
An informational expert is another principle that helps us decide to which class a method, an attribute, or other class member belongs. The class with the most information always has the responsibility. We call this class the information expert, and we add more functionality and data to it. We've already talked about a similar principle with the Creator pattern.
Low coupling
Low coupling is basically the same thing as High cohesion but from another point of view. In our application, we should create the smallest number of references between objects as possible. We can achieve it by distributing the responsibility wisely.
As a deterrent example, let's mention a Manager
class, which
would include the logic for working with customers, invoices, logistics, simply
for everything. Such objects are sometimes called "god objects", they have too
much responsibility and therefore create too many coupling (such a
Manager
would typically use a large number of classes to work like
this). The total number of references in the application is not that important,
it's the number of references between objects what matters. We always try to
make the class communicates with as few other classes as possible, so we should
add other managers such as UsersManager
,
InvoiceManager
, LogisticsManager
and others. You
probably already figured out that such a manager would not be very reusable in
other application as well.
A deterrent example of a god object not respecting Low coupling
And we don't just have to stick with classes. Low coupling is also related
to, for example, other practices such as method naming ("We should name methods
with as fewest words as possible and without the AND conjunction"). The
do()
or parseAndProcessRecord()
methods suggest that
they do too much.
Note: Since we've already mentioned god objects, let's mention the opposite problem, which is called the Yoyo problem. With too little program structures, the granularity too high, often even due to overuse of inheritance, there are so many small classes in the program. The programmer then must keep switching from class to class, to find out how it works and then go back. This action may resemble throwing yoyo down and up, over and over again. For this reason, object composition is often preferred over inheritance.
Talking about references between objects, we should also avoid
cyclical references, which are generally considered to be a bad
practice. This happens when the A
class refers to the
B
class which refers back to the A
class. If this
happens, there's something wrong with the design. Cyclical references can be
also created through multiple classes.
Polymorphism
Yes, even polymorphism is a design pattern. Although the principle of
polymorphism should be well known to you, let's repeat it for the sake of
completeness. Most often, it's the case when a descendant modifies the
behavior of its ancestor but keeps its interface. From the programming
point of view, we're talking about overriding parent methods. We can then work
with the objects using a unified interface, but each object modifies the
inherited functionality by itself. Polymorphism isn't limited only to
inheritance, it's generally about working with objects of different types using
a common interface they all implement. Let's show the popular example of animals
each having a talk()
method, but overriding it from the
Animal
ancestor to make their specific sounds:
If you want a more realistic example of polymorphism, there can be a common
ancestor for all form controls. Every descendant would override the ancestor's
methods such as draw()
, getSize()
and similar
depending on their specific functionality.
Protected variations
The practice talks about creating a stable interface at the key
places of the application, where the modification of the interface would cause
the need to overwrite a large part of the application. Let's discuss a
real example again. In ICT.social's information system, we use the Protected
variations principle, specifically, the "Adapter design pattern" and that's how
we prevent changes which Facebook makes in their API. Allowing registrations via
Facebook and similar integration result in an increase in the number and
activity of our users, but unfortunately at the cost of rewriting the
application every few months. Using the FacebookManagerInterface
interface, our system doesn't have to change anymore. When the new version comes
out and Facebook redoes everything, we'll just implement this interface by a new
manager (such as FacebookManagerXX
, where XX is the Facebook API
version), and we'll pass the new instance implementing this interface to the
system. Of course, the interface can also be defined by polymorphism and an
abstract classes.
Pure fabrication
We have already talked about Pure fabrication today. It represents classes that only serve to simplify the design of the system. Just as Controller was actually a case of Indirection, Indirection is a case of Pure fabrication. Service classes out of the application functionality reduce the number of dependencies and increase coherence.
That would be all of GRASP and I look forward to seeing you with other online courses on ICT.social.