Introduction

I recently noticed that a lot of attention is paid to SOLID principles. And this is very good thing because it is the total basis of Object-Oriented Design (OOD) and programming. For developers of object-oriented languages, knowledge of the SOLID principles is a requirement for writing code which is characterized by good quality. There are a lot of articles and courses on these rules, so if you do not know them yet, learn them as soon as possible.

On the other hand, there is another, less well-known set of rules regarding object-oriented programming. It’s called GRASP – General Responsibility Assignment Software Patterns (or Principles). There are far fewer materials on the Internet about this topic, so I decided to bring it closer because I think the principles described in it are as important as the SOLID principles.

Disclaimer : This post is inspired and based on awesome Craig Larman’s book: Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development. Although the last edition was released in 2004, according to me, the book is still up-to-date and explains perfectly how to design systems using object-oriented languages. It’s hard to find the better book about this subject, believe me. It is not book about UML, but you can learn UML from it because it is good explained too. Must-have for each developer, period.

The Responsibility in Software

Responsibility in software is a very important concept and not only concerns classes but also modules and entire systems. Thinking in terms of responsibilities is popular way to think about design of the software. We can always ask questions like:



– What is the responsibility of this class/module/component/system?

– Is it responsible for this or is it responsible for that?

– Is Single Responsibility Principle violated in this particular context?

But to answer these kind of questions we should ask one, more fundamental question: what does it mean that something is responsible in the context of the software?

Doing and Knowing

As it is proposed by Rebecca Wirfs-Brock in Object Design: Roles, Responsibilities, and Collaborations book and her RDD approach a responsibility is:

an obligation to perform a task or know information

As we see from this definition we have here a clear distinction between behavior (doing) and data (knowing).

Doing responsibility of an object is seen as:

a) doing something itself – create an object, process data, do some computation/calculation

b) initiate and coordinate actions with other objects

Knowing responsibility of an object can be defined as:

a) private and public object data

b) related objects references

c) things it can derive

Let’s see an example:

Knowing and doing responsibilities public class Customer : Entity, // knowing IAggregateRoot // knowing { public Guid Id { get; private set; } // knowing public string Email { get; private set; } // knowing public string Name { get; private set; } // knowing private readonly List<Order> _orders; // knowing private Customer() { this._orders = new List<Order>(); } // doing something itself public Customer(string email, string name, ICustomerUniquenessChecker customerUniquenessChecker) { this.Email = email; this.Name = name; // doing - initiate and coordinate actions with other objects var isUnique = customerUniquenessChecker.IsUnique(this); if (!isUnique) { throw new BusinessRuleValidationException("Customer with this email already exists."); } this.AddDomainEvent(new CustomerRegisteredEvent(this)); } // doing something itself public void AddOrder(Order order) { // doing - initiate and coordinate actions with other objects if (this._orders.Count(x => x.IsOrderedToday()) >= 2) { throw new BusinessRuleValidationException("You cannot order more than 2 orders on the same day"); } this._orders.Add(order); this.AddDomainEvent(new OrderAddedEvent(order)); } // doing something itself public void ChangeOrder( Guid orderId, List<OrderProduct> products, List<ConversionRate> conversionRates) { var order = this._orders.Single(x => x.Id == orderId); // doing - initiate and coordinate actions with other objects order.Change(products, conversionRates); this.AddDomainEvent(new OrderChangedEvent(order)); } // doing something itself public void RemoveOrder(Guid orderId) { var order = this._orders.Single(x => x.Id == orderId); // doing - initiate and coordinate actions with other objects order.Remove(); this.AddDomainEvent(new OrderRemovedEvent(order)); } // doing something itself public GetOrdersTotal(Guid orderId) { return this._orders.Sum(x => x.Value); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 public class Customer : Entity , // knowing IAggregateRoot // knowing { public Guid Id { get ; private set ; } // knowing public string Email { get ; private set ; } // knowing public string Name { get ; private set ; } // knowing private readonly List < Order > _orders ; // knowing private Customer ( ) { this . _orders = new List < Order > ( ) ; } // doing something itself public Customer ( string email , string name , ICustomerUniquenessChecker customerUniquenessChecker ) { this . Email = email ; this . Name = name ; // doing - initiate and coordinate actions with other objects var isUnique = customerUniquenessChecker . IsUnique ( this ) ; if ( ! isUnique ) { throw new BusinessRuleValidationException ( "Customer with this email already exists." ) ; } this . AddDomainEvent ( new CustomerRegisteredEvent ( this ) ) ; } // doing something itself public void AddOrder ( Order order ) { // doing - initiate and coordinate actions with other objects if ( this . _orders . Count ( x = > x . IsOrderedToday ( ) ) >= 2 ) { throw new BusinessRuleValidationException ( "You cannot order more than 2 orders on the same day" ) ; } this . _orders . Add ( order ) ; this . AddDomainEvent ( new OrderAddedEvent ( order ) ) ; } // doing something itself public void ChangeOrder ( Guid orderId , List < OrderProduct > products , List < ConversionRate > conversionRates ) { var order = this . _orders . Single ( x = > x . Id == orderId ) ; // doing - initiate and coordinate actions with other objects order . Change ( products , conversionRates ) ; this . AddDomainEvent ( new OrderChangedEvent ( order ) ) ; } // doing something itself public void RemoveOrder ( Guid orderId ) { var order = this . _orders . Single ( x = > x . Id == orderId ) ; // doing - initiate and coordinate actions with other objects order . Remove ( ) ; this . AddDomainEvent ( new OrderRemovedEvent ( order ) ) ; } // doing something itself public GetOrdersTotal ( Guid orderId ) { return this . _orders . Sum ( x = > x . Value ) ; } }

If you want more information about responsibilities in software and dig into Responsibility-Driven Design you can read it directly from Rebecca’s Wirfs-Brock book or this PDF.

Ok, now we know what the responsibility in context of software is. Let’s see how to assign this responsibility using GRASP.

GRASP

GRASP is set of exactly 9 General Responsibility Assignment Software Patterns. As I wrote above assignment of object responsibilities is one of the key skill of OOD. Every programmer and designer should be familiar with these patterns and what is more important – know how to apply them in everyday work (by the way – the same assumptions should apply to SOLID principles).

This is the list of 9 GRASP patterns (sometimes called principles but please, do not focus on naming here):

1. Information Expert

2. Creator

3. Controller

4. Low Coupling

5. High Cohesion

6. Indirection

7. Polymorphism

8. Pure Fabrication

9. Protected Variations

NOTE : All Problem/Solution paragraphas are quotes from Craig Larman’s book. I decided that it would be best to stick to the original.

1. Information Expert

Problem: What is a basic principle by which to assign responsibilities to objects?

Solution: Assign a responsibility to the class that has the information needed to fulfill it.

In following example Customer class has references to all customer Orders so it is natural candidate to take responsibility of calculating total value of orders:

Information Expert example public class Customer : Entity, IAggregateRoot { private readonly List<Order> _orders; public GetOrdersTotal(Guid orderId) { return this._orders.Sum(x => x.Value); } } 1 2 3 4 5 6 7 8 9 public class Customer : Entity , IAggregateRoot { private readonly List < Order > _orders ; public GetOrdersTotal ( Guid orderId ) { return this . _orders . Sum ( x = > x . Value ) ; } }

This is the most basic principle, because the truth is – if we do not have the data we need, we would not be able to meet the requirement and assign responsibility anyway.

2. Creator

Problem: Who creates object A?

Solution: Assign class B the responsibility to create object A if one of these is true (more is better)

– B contains or compositely aggregates A

– B records A

– B closely uses A

– B has the initializing data for A

Going back to the example:

Creator public class Customer : Entity, IAggregateRoot { private readonly List<Order> _orders; public void AddOrder(List<OrderProduct> orderProducts) { var order = new Order(orderProducts); // Creator if (this._orders.Count(x => x.IsOrderedToday()) >= 2) { throw new BusinessRuleValidationException("You cannot order more than 2 orders on the same day"); } this._orders.Add(order); this.AddDomainEvent(new OrderAddedEvent(order)); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Customer : Entity , IAggregateRoot { private readonly List < Order > _orders ; public void AddOrder ( List < OrderProduct > orderProducts ) { var order = new Order ( orderProducts ) ; // Creator if ( this . _orders . Count ( x = > x . IsOrderedToday ( ) ) >= 2 ) { throw new BusinessRuleValidationException ( "You cannot order more than 2 orders on the same day" ) ; } this . _orders . Add ( order ) ; this . AddDomainEvent ( new OrderAddedEvent ( order ) ) ; } }

As you can see above Customer class compositely aggregates Orders (there is no Order without Customer), records Orders, closely uses Orders and has initializing data passed by method parameters. Ideal candidate for “Order Creator”. 🙂

3. Controller

Problem: What first object beyond the UI layer receives and coordinates “controls” a system operation?

Solution: Assign the responsibility to an object representing one of these choices:

– Represents the overall “system”, “root object”, device that the software is running within, or a major subsystem (these are all variations of a facade controller)

– Represents a use case scenario within which the system operation occurs (a use case or session controller)

This principle implementation depends on high level design of our system but general we need always define object which orchestrate our business transaction processing. At first glance, it would seem that the MVC Controller in Web applications/API’s is a great example here (even the name is the same) but for me it is not true. Of course it receives input but it shouldn’t coordinate a system operation – it should delegate it to separate service or Command Handler:

Controller public class CustomerOrdersController : Controller { private readonly IMediator _mediator; public CustomerOrdersController(IMediator mediator) { this._mediator = mediator; } /// <summary> /// Add customer order. /// </summary> /// <param name="customerId">Customer ID.</param> /// <param name="request">Products list.</param> [Route("{customerId}/orders")] [HttpPost] [ProducesResponseType((int)HttpStatusCode.Created)] public async Task<IActionResult> AddCustomerOrder( [FromRoute]Guid customerId, [FromBody]CustomerOrderRequest request) { await _mediator.Send(new AddCustomerOrderCommand(customerId, request.Products)); return Created(string.Empty, null); } } public class AddCustomerOrderCommandHandler : IRequestHandler<AddCustomerOrderCommand> { private readonly ICustomerRepository _customerRepository; private readonly IProductRepository _productRepository; private readonly IForeignExchange _foreignExchange; public AddCustomerOrderCommandHandler( ICustomerRepository customerRepository, IProductRepository productRepository, IForeignExchange foreignExchange) { this._customerRepository = customerRepository; this._productRepository = productRepository; this._foreignExchange = foreignExchange; } public async Task<Unit> Handle(AddCustomerOrderCommand request, CancellationToken cancellationToken) { // handling... } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class CustomerOrdersController : Controller { private readonly IMediator _mediator ; public CustomerOrdersController ( IMediator mediator ) { this . _mediator = mediator ; } /// <summary> /// Add customer order. /// </summary> /// <param name="customerId">Customer ID.</param> /// <param name="request">Products list.</param> [ Route ( "{customerId}/orders" ) ] [ HttpPost ] [ ProducesResponseType ( ( int ) HttpStatusCode . Created ) ] public async Task < IActionResult > AddCustomerOrder ( [ FromRoute ] Guid customerId , [ FromBody ] CustomerOrderRequest request ) { await _mediator . Send ( new AddCustomerOrderCommand ( customerId , request . Products ) ) ; return Created ( string . Empty , null ) ; } } public class AddCustomerOrderCommandHandler : IRequestHandler < AddCustomerOrderCommand > { private readonly ICustomerRepository _customerRepository ; private readonly IProductRepository _productRepository ; private readonly IForeignExchange _foreignExchange ; public AddCustomerOrderCommandHandler ( ICustomerRepository customerRepository , IProductRepository productRepository , IForeignExchange foreignExchange ) { this . _customerRepository = customerRepository ; this . _productRepository = productRepository ; this . _foreignExchange = foreignExchange ; } public async Task < Unit > Handle ( AddCustomerOrderCommand request , CancellationToken cancellationToken ) { // handling... } }

4. Low Coupling

Problem: How to reduce the impact of change? How to support low dependency and increased reuse?

Solution: Assign responsibilities so that (unnecessary) coupling remains low. Use this principle to evaluate alternatives.

Coupling is a measure how one element is related to another. The higher the coupling, the greater the dependence of one element to the another.

Low coupling means our objects are more independent and isolated. If something is isolated we can change it not worrying that we have to change something else or wheter we would break something (see Shotgun Surgery). Use of SOLID principles are great way to keep coupling low. As you see in example above between CustomerOrdersController and AddCustomerOrderCommandHandler coupling remains low – they need only agree on command object structure. This low coupling is possible thanks to Indirection pattern which is described later.

5. High Cohesion

Problem: How to keep objects focused, understandable, manageable and as a side effect support Low Coupling?

Solution: Assign a responsibility so that cohesion remains high. Use this to evaluate alternatives.

Cohesion is a measure how strongly all responsibilities of the element are related. In other words, what is the degree to which the parts inside a element belong together.

Classes with low cohesion have unrelated data and/or unrelated behaviors. For example, the Customer class has high cohesion because now it does only one thing – manage the Orders. If I would add to this class management of product prices responsibility, cohesion of this class would drop significantly because price list is not directly related to Customer itself.

6. Indirection

Problem: Where to assign a responsibility to avoid direct coupling between two or more things?

Solution: Assign the responsibility to an intermediate object to mediate between other components or services so that they are not directly coupled.

This is where Mediator Pattern comes in to play. Instead of direct coupling:

Direct coupling public class CustomerOrdersController : Controller { private readonly IOrdersService _ordersService; public CustomerOrdersController(IOrdersService ordersService) { this._ordersService = ordersService; } } 1 2 3 4 5 6 7 8 9 public class CustomerOrdersController : Controller { private readonly IOrdersService _ordersService ; public CustomerOrdersController ( IOrdersService ordersService ) { this . _ordersService = ordersService ; } }

we can use the mediator object and mediate between objects:

Mediation between objects public class CustomerOrdersController : Controller { private readonly IMediator _mediator; public CustomerOrdersController(IMediator mediator) { this._mediator = mediator; } public async Task<IActionResult> AddCustomerOrder( [FromRoute]Guid customerId, [FromBody]CustomerOrderRequest request) { await _mediator.Send(new AddCustomerOrderCommand(customerId, request.Products)); return Created(string.Empty, null); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class CustomerOrdersController : Controller { private readonly IMediator _mediator ; public CustomerOrdersController ( IMediator mediator ) { this . _mediator = mediator ; } public async Task < IActionResult > AddCustomerOrder ( [ FromRoute ] Guid customerId , [ FromBody ] CustomerOrderRequest request ) { await _mediator . Send ( new AddCustomerOrderCommand ( customerId , request . Products ) ) ; return Created ( string . Empty , null ) ; } }

One note here. Indirection supports low coupling but reduces readability and reasoning about the whole system. You don’t know which class handles the command from the Controller definition. This is the trade-off to take into consideration.

7. Polymorphism

Problem: How handle alternatives based on type?

Solution: When related alternatives or behaviors vary by type (class), assingn responsibility for the behavior (using polymorphi operations) to the types for which the behavior varies.

Polymorphism is fundamental principle of Object-Oriented Design. In this context, principle is strongly connected with (among others) Strategy Pattern.

As it was presented above constructor of Customer class takes ICustomerUniquenessChecker interface as parameter:

ICustomerUniquenessChecker in Customer constructor parameter public Customer(string email, string name, ICustomerUniquenessChecker customerUniquenessChecker) { this.Email = email; this.Name = name; var isUnique = customerUniquenessChecker.IsUnique(this); // doing - initiate and coordinate actions with other objects if (!isUnique) { throw new BusinessRuleValidationException("Customer with this email already exists."); } this.AddDomainEvent(new CustomerRegisteredEvent(this)); } 1 2 3 4 5 6 7 8 9 10 11 12 13 public Customer ( string email , string name , ICustomerUniquenessChecker customerUniquenessChecker ) { this . Email = email ; this . Name = name ; var isUnique = customerUniquenessChecker . IsUnique ( this ) ; // doing - initiate and coordinate actions with other objects if ( ! isUnique ) { throw new BusinessRuleValidationException ( "Customer with this email already exists." ) ; } this . AddDomainEvent ( new CustomerRegisteredEvent ( this ) ) ; }

We can provide there different implementations of this interface depending on the requirements. In general, this is very useful approach when we have in our systems different algorithms that have the same input and output (in terms of structure).

8. Pure Fabrication

Problem: What object should have the responsibility, when you do not want to viloate High Cohesion and Low Coupling but solutions offered by other principles are not appopriate?>

Solution: Assign a highly cohesive set of responsibilites to an artifical or convenience class that does not represent a problem domain conecept.

Sometimes it is realy hard to figure it out where responsibility should be placed. This is why in Domain-Driven Design there is a concept of Domain Service. Domain Services hold logic which are not related with one, particular Entity.

For example, in e-commerce systems we often have need to convert one currency to another. Sometimes it is hard to say where this behavior should be placed so the best option is to create new class and interface:

Pure fabrication public interface IForeignExchange { List<ConversionRate> GetConversionRates(); } public class ForeignExchange : IForeignExchange { private readonly ICacheStore _cacheStore; public ForeignExchange(ICacheStore cacheStore) { _cacheStore = cacheStore; } public List<ConversionRate> GetConversionRates() { var ratesCache = this._cacheStore.Get(new ConversionRatesCacheKey()); if (ratesCache != null) { return ratesCache.Rates; } List<ConversionRate> rates = GetConversionRatesFromExternalApi(); this._cacheStore.Add(new ConversionRatesCache(rates), new ConversionRatesCacheKey(), DateTime.Now.Date.AddDays(1)); return rates; } private static List<ConversionRate> GetConversionRatesFromExternalApi() { // Communication with external API. Here is only mock. var conversionRates = new List<ConversionRate>(); conversionRates.Add(new ConversionRate("USD", "EUR", (decimal)0.88)); conversionRates.Add(new ConversionRate("EUR", "USD", (decimal)1.13)); return conversionRates; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public interface IForeignExchange { List < ConversionRate > GetConversionRates ( ) ; } public class ForeignExchange : IForeignExchange { private readonly ICacheStore _cacheStore ; public ForeignExchange ( ICacheStore cacheStore ) { _cacheStore = cacheStore ; } public List < ConversionRate > GetConversionRates ( ) { var ratesCache = this . _cacheStore . Get ( new ConversionRatesCacheKey ( ) ) ; if ( ratesCache != null ) { return ratesCache . Rates ; } List < ConversionRate > rates = GetConversionRatesFromExternalApi ( ) ; this . _cacheStore . Add ( new ConversionRatesCache ( rates ) , new ConversionRatesCacheKey ( ) , DateTime . Now . Date . AddDays ( 1 ) ) ; return rates ; } private static List < ConversionRate > GetConversionRatesFromExternalApi ( ) { // Communication with external API. Here is only mock. var conversionRates = new List < ConversionRate > ( ) ; conversionRates . Add ( new ConversionRate ( "USD" , "EUR" , ( decimal ) 0.88 ) ) ; conversionRates . Add ( new ConversionRate ( "EUR" , "USD" , ( decimal ) 1.13 ) ) ; return conversionRates ; } }

This way we support both High Cohesion (we are only converting currencies) and Low Coupling (client classes are only dependent to IForeignExchange interface). Additionally, this class is reusable and easy to maintain.

9. Protected Variations

Problem: How to design objects, subsystems and systems so that the variations or instability in these elements does not have an undesirable impact on other elements?

Solution: Identify points of predicted variation or instability, assign responsibilities to create a stable interface around them.

In my opinion, this is the most important principle which is indirectly related to the rest GRASP principles. Currently, one of the most important software metrics is the ease of change. As architects and programmers we must be ready for ever-changing requirements. This is not optional and “nice to have” quality attribute – it is “must-have” and our duty .

Fortunately, we are armed with a lot design guidelines, principles, patterns and practices to support changes on different levels of abstraction. I will mention only a few (already beyond the GRASP):

– SOLID principles, especially the Open-Close principle (but all of them supports change)

– Gang of Four (GoF) Design Patterns

– Encapsulation

– Law of Demeter

– Service Discovery

– Virtualization and containerization

– asynchronous messaging, Event-driven architectures

– Orchestration, Choreography

As iterative software development process is more suitable today because even we are forced to change something once, we can draw conclusions and be prepared for future changes at a lower cost.

Fool me once shame on you. Fool me twice shame on me.

Summary

In this post I described one of the most fundamental Object-Oriented Design set of patterns and principles – GRASP.

Skilful management of responsibilities in software is the key to create good quality architecture and code. In combination with others patterns and practices is it possible to develop well-crafted systems which supports change and do not resist it. This is good, because the only thing that is certain is change. So be prepared.

Related posts

10 common broken rules of clean code