GRASP (General Responsibility Assignment Software Patterns)
GRASP (General Responsibility Assignment Software Patterns) Principle in Programming
GRASP includes 9 basic principles that help properly distribute responsibilities in code. Here is each of them explained in detail with examples.
Principle: Responsibility should be assigned to the class that has sufficient information to fulfill it.
Bad Example
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
}
class OrderProcessor {
calculateTotal(order: Order): number {
let total = 0;
for (const item of order.items) {
total += item;
}
return total;
}
}
Problem: The OrderProcessor is doing something it doesn't have sufficient data for. It would be more logical for the Order itself to have that logic.
Good Example
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item, 0);
}
}
Now the Order has its own data and can independently calculate the total.
Principle: If one object uses another object and depends on it, it should also create it.
Example
class Engine {}
class Car {
engine: Engine;
constructor() {
this.engine = new Engine(); // Car creates Engine because it uses it
}
}
The Car creates the Engine because it is a component of it.
Principle: The Controller should be an object that manages use cases and serves as a mediating interface.
Example
class OrderController {
placeOrder(items: number[]) {
const order = new Order(items);
console.log("Order placed:", order.calculateTotal());
}
}
const controller = new OrderController();
controller.placeOrder([10, 20, 30]); // The Controller manages the logic
Now the OrderController manages the creation of the Order.
Principle: Objects should have minimal dependencies on each other to make the system more flexible.
Bad Example
class Order {
paymentProcessor: PaymentProcessor;
constructor() {
this.paymentProcessor = new PaymentProcessor(); // Direct dependency
}
processPayment() {
this.paymentProcessor.process();
}
}
Problem: The Order is directly dependent on the PaymentProcessor. If it changes, the Order must also change.
Good Example
class Order {
processPayment(paymentProcessor: PaymentProcessor) {
paymentProcessor.process(); // Dependency is provided externally
}
}
Now the Order is more independent, and we can pass any payment processor.
Principle: Methods in a class should be related to each other and not contain redundant functionality.
Bad Example
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
calculateTotal() { /* ... */ }
printReceipt() { /* ... */ } // Should be in a separate class
}
Problem: The Order performs redundant functionality (printReceipt), which should be in another class.
Good Example
class ReceiptPrinter {
print(order: Order) { /* ... */ }
}
Now the Order does not perform redundant functionality.
Principle: Different types of objects should be able to be used in the same way without knowing their specific type.
Example
interface PaymentMethod {
processPayment(amount: number): void;
}
class CreditCardPayment implements PaymentMethod {
processPayment(amount: number) {
console.log(`Paid ${amount} with Credit Card`);
}
}
class PayPalPayment implements PaymentMethod {
processPayment(amount: number) {
console.log(`Paid ${amount} with PayPal`);
}
}
class Order {
processPayment(method: PaymentMethod) {
method.processPayment(100);
}
}
Now the Order can accept any payment method without changes.
Principle: When logic does not belong to any specific role, it should be moved to a separate class without violating object-oriented design.
Bad Example (Data-holding object also writes to a file)
class Report {
generate() {
return "This is a report";
}
saveToFile() {
console.log("Saving report to file..."); // Logic should be elsewhere
}
Problem: The Report should only handle report generation, not writing to files.
Good Example
class Report {
generate() {
return "This is a report";
}
}
class FileSaver {
save(content: string) {
console.log("Saving to file:", content);
}
}
const report = new Report();
const fileSaver = new FileSaver();
fileSaver.save(report.generate()); // Now the logic is separated
Now the Report does not have redundant functionality, and file saving is handled by the FileSaver.
Principle: When low coupling is needed between two parts, a mediator or another model is used to manage their interaction.
Bad Example (Direct coupling)
class UserService {
getUser() {
return { id: 1, name: "John Doe" };
}
}
class Profile {
userService: UserService;
constructor() {
this.userService = new UserService(); // Direct dependency
}
showProfile() {
const user = this.userService.getUser();
console.log("User:", user.name);
}
}
Problem: The Profile is directly dependent on the UserService, making changes and testing difficult.
Good Example
interface IUserService {
getUser(): { id: number; name: string };
}
class UserService implements IUserService {
getUser() {
return { id: 1, name: "John Doe" };
}
}
class Profile {
userService: IUserService;
constructor(userService: IUserService) {
this.userService = userService; // Indirection, now it's easier to change `UserService`
}
showProfile() {
const user = this.userService.getUser();
console.log("User:", user.name);
}
}
const userService = new UserService();
const profile = new Profile(userService);
profile.showProfile();
Now the Profile is not directly dependent on a specific UserService but only on the IUserService interface. This allows easy replacement of UserService, for example, with a mock during testing.
Principle: Elements that are likely to change should be protected through interfaces, abstractions, or design patterns.
Bad Example (Changing dependency)
class MySQLDatabase {
query(sql: string) {
console.log(`Executing query on MySQL: ${sql}`);
}
}
class UserRepository {
db: MySQLDatabase;
constructor() {
this.db = new MySQLDatabase(); // Direct dependency
}
getUser(id: number) {
this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
Problem: If we decide to use PostgreSQL tomorrow, the code will need to be completely rewritten.
Good Example
interface Database {
query(sql: string): void;
}
class MySQLDatabase implements Database {
query(sql: string) {
console.log(`Executing query on MySQL: ${sql}`);
}
}
class PostgreSQLDatabase implements Database {
query(sql: string) {
console.log(`Executing query on PostgreSQL: ${sql}`);
}
}
class UserRepository {
db: Database;
constructor(db: Database) {
this.db = db; // Variable option. Transition from MySQL to PostgreSQL is easy
}
getUser(id: number) {
this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
const db = new MySQLDatabase();
const userRepository = new UserRepository(db);
userRepository.getUser(1);
Now the UserRepository is protected from switching from MySQL to PostgreSQL because it is not directly dependent on a specific database.
Principle | Purpose |
Information Expert | The object that has the data should implement the functionality working with it. |
Creator | If one object uses another, it should also create it. |
Controller | Controllers should manage use cases. |
Low Coupling | Objects should have minimal dependencies on each other. |
High Cohesion | Objects should have only responsibilities related to them. |
Polymorphism | Code should use interfaces or abstractions to ensure extensibility. |
Pure Fabrication | Specialized logic should be separated into a separate class. |
Indirection | Dependencies should be reduced through mediation. |
Protected Variations | Elements that are likely to change should be protected through interfaces or abstractions. |