0 437 en

GRASP Principles: 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

GRASP: Information Expert
GRASP: 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

GRASP: Creator
GRASP: 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

GRASP: Controller
GRASP: 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

GRASP: Low Coupling
GRASP: 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

GRASP: 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

GRASP: Polymorphism
GRASP: 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

GRASP: Indirection
GRASP: 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

GRASP: Protected Variations
GRASP: 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.

Comments:

Please log in to be able add comments.