GRASP: General Responsibility Assignment Software Patterns
GRASP Principles provides guidelines for making better decisions in object-oriented design. Craig Larman introduced nine GRASP patterns in his book Applying UML and Patterns. Unlike the classic 'Gang of Four' patterns, GRASP focuses on general approaches rather than specific solutions.
Information Expert
Assign responsibility to the class that has the necessary information to fulfill it.
Why It’s Useful: This minimizes dependencies between classes and makes the code easier to read and maintain.
Information Expert C# Example:
For an online store that calculates the total price of an order, the Order
class should calculate it because it already has the details about items and quantities.
public class Order
{
public List<OrderItem> Items { get; set; }
public decimal CalculateTotalPrice()
{
return Items.Sum(item => item.Price * item.Quantity);
}
}
The Order
class is the Information Expert, which has the necessary data to calculate the total price.
Creator
Assign responsibility for creating an object to a class with the information needed to initialize it or have a close relationship with it.
Why It’s Useful: It logically links related objects, reducing unnecessary and disconnected instances.
Creator C# Example:
For example, the Customer
class can create an Order
object since a customer is logically responsible for placing orders.
public class Customer
{
public Order CreateOrder()
{
var order = new Order { Customer = this };
return order;
}
}
The Customer
class creates an Order
object, demonstrating the Creator principle as it logically has the responsibility to initiate an order.
Controller
Assign responsibility for handling system events or coordinating activities to classes that represent key actors or system boundary objects.
Why It’s Useful: Keeps user action handling logic separate from core business logic, making code more modular.
Controller C# Example:
For an e-commerce site, OrderController
handles the order submission, coordinates with other objects (Order
, Payment
), and triggers actions.
public class OrderController
{
public void SubmitOrder(Order order)
{
// Validate the order
if (order.IsValid())
{
// Coordinate with payment processing
PaymentProcessor paymentProcessor = new PaymentProcessor();
paymentProcessor.ProcessPayment(order);
// Save the order
OrderRepository repository = new OrderRepository();
repository.SaveOrder(order);
}
}
}
Low Coupling
Minimize dependencies between classes to reduce the impact of changes and make the system easier to understand.
Keep relationships between classes minimal and controlled by using:
- Use Interfaces: Classes depend on abstractions (interfaces), not concrete implementations, for easier substitution. Also, we can refer to the Dependency Inversion Principle (DIP) from SOLID
- Dependency Injection: Inject dependencies instead of creating them internally, which makes testing and changing implementations easier.
- Separation of Concerns: Ensure each class has one responsibility to avoid unnecessary dependencies. Also, we can refer to the Single Responsibility Principle (SRP) from SOLID
Why It’s Useful: Reduces the impact of changes, making code easier to update and maintain.
Low Coupling C# Example:
The Order
class uses OrderRepository
to interact with the database, reducing direct dependencies.
public class Order
{
private readonly OrderRepository _orderRepository;
public Order(OrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void SaveOrder()
{
_orderRepository.Save(this);
}
}
public class OrderRepository
{
public void Save(Order order)
{
// Logic to save the order to the database
}
}
High Cohesion
Keep related responsibilities within a single class or module to ensure clarity and maintainability.
Why It’s Useful: High Cohesion makes classes easier to understand, test, and update.
High Cohesion C# Example
The Order
class maintains high cohesion because all its methods and properties directly relate to managing orders, like adding items and calculating the total price.
public class Order
{
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
public void AddItem(OrderItem item)
{
Items.Add(item);
}
public decimal CalculateTotalPrice()
{
return Items.Sum(item => item.Price * item.Quantity);
}
}
Polymorphism
Use polymorphism to assign responsibilities for behaviors that may change depending on the type.
Why It’s Useful: Allows for scalable and flexible code, where adding new types doesn’t require changes to existing parts.
Polymorphism C# Example:
A payment system could use polymorphism for CreditCardPayment
, PayPalPayment
. These classes implement a common IPaymentMethod
interface, each handling payments differently.
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
}
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}
public class PayPalPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount}");
}
}
public class PaymentProcessor
{
public void Process(IPaymentMethod paymentMethod, decimal amount)
{
paymentMethod.ProcessPayment(amount);
}
}
In this example, different payment methods (CreditCardPayment
, PayPalPayment
) implement the IPaymentMethod
interface. The PaymentProcessor
can work with any payment method without knowing the specific type, making the code scalable and easy to extend.
Pure Fabrication
Create a new class to handle responsibilities that don’t naturally fit into existing classes.
Why It’s Useful: Keeps the codebase clean and maintains high cohesion by avoiding unrelated methods in domain classes.
Pure Fabrication C# Example:
NotificationService
is created to handle sending notifications, keeping this responsibility separate from the Order
class. This ensures high cohesion and a clean separation of concerns.
public class NotificationService
{
public void SendNotification(string message)
{
Console.WriteLine($"Sending notification: {message}");
}
}
public class Order
{
private readonly NotificationService _notificationService;
public Order(NotificationService notificationService)
{
_notificationService = notificationService;
}
public void CompleteOrder()
{
// Complete order logic
_notificationService.SendNotification("Order has been completed.");
}
}
Indirection
Use an intermediary class to mediate between two classes to reduce coupling.
The mediator can be a component, an interface, an abstract class, or a design pattern. The idea is to hide the details and variations of the actual classes behind a common abstraction that defines their roles and contracts. This way, the classes depend only on the abstraction, not the concrete implementation.
Why It’s Useful: Minimizes direct dependencies and makes changing how two classes interact easier.
Indirection is a key technique in object-oriented design. Examples of indirection represented in GoF patterns include:
- Facade Pattern: Simplifies access to a complex system by providing a straightforward interface.
- Adapter Pattern: Bridges incompatible classes by converting one interface into another.
- Proxy Pattern: Acts as a placeholder to control access or add functionality to another object.
- Observer Pattern: Establishes a one-to-many relationship, where observers are notified of changes in a subject.
These patterns manage complexity and enhance flexibility in design.
C# Example:
In this example, the Order
class uses the PaymentProcessor
to handle complex payment operations, such as authentication, authorization, and transaction execution, which reduces coupling by preventing Order
from directly depending on the payment gateway and its intricate logic.
public class PaymentProcessor
{
private readonly IExternalPaymentGateway _externalPaymentGateway;
public PaymentProcessor(IExternalPaymentGateway externalPaymentGateway)
{
_externalPaymentGateway = externalPaymentGateway;
}
public void ProcessPayment(Order order)
{
_externalPaymentGateway.Authenticate();
_externalPaymentGateway.Authorize();
_externalPaymentGateway.ValidatePaymentDetails(order);
_externalPaymentGateway.ExecuteTransaction();
_externalPaymentGateway.ConfirmTransaction();
Console.WriteLine("Processing payment for order " + order.OrderNumber);
}
}
public class Order
{
public string OrderNumber { get; set; }
public void CompleteOrder(PaymentProcessor paymentProcessor)
{
// Use PaymentProcessor instead of interacting directly with payment gateway
paymentProcessor.ProcessPayment(this);
// other logic like send notification, etc.
}
}
Protected Variations
Design the system to protect against variations or changes that may affect other parts of the code.
Why It’s Useful: Isolates changes, making the system more robust and easier to modify.
Protected Variations C# Example:
Instead of calling an API directly, create an interface like IDataProvider
, it helps easily substitute another implementation like SQLDataProvider
if needed.
public interface IDataProvider
{
string GetData();
}
public class ApiDataProvider : IDataProvider
{
public string GetData()
{
// Call the API and return data
return "API data";
}
}
public class DataConsumer
{
private readonly IDataProvider _dataProvider;
public DataConsumer(IDataProvider dataProvider)
{
_dataProvider = dataProvider;
}
public void DisplayData()
{
string data = _dataProvider.GetData();
Console.WriteLine("Data: " + data);
}
}
DataConsumer
depends on the IDataProvider
interface rather than a specific implementation. This allows you to change the implementation, e.g., replace ApiDataProvider
with a mock provider for testing without affecting the rest of the code.
Summary
The GRASP principles provide a foundation for assigning responsibilities in an object-oriented system. Here’s a quick recap:
- Information Expert: Assign responsibility to the class with the necessary information.
- Creator: Assign object creation to the class most closely related to it.
- Controller: Use a controller class to handle user actions.
- Low Coupling: Minimize dependencies between classes.
- High Cohesion: Keep related responsibilities together in a single class.
- Polymorphism: Use polymorphism to handle behavior variations.
- Pure Fabrication: Create utility classes when needed.
- Indirection: Use intermediaries to reduce coupling.
- Protected Variations: Design to isolate parts that may change.
Applying GRASP principles helps write cleaner, more modular, and maintainable code, essential for scalable software development.