In the world of software development, design patterns serve as time-tested solutions to common problems.
One of the less commonly discussed but equally vital sets of design patterns is GRASP (General Responsibility Assignment Software Patterns). Principles of GRASP often correlate with SOLID principles and other OOP design patterns.
GRASP, or General Responsibility Assignment Software Patterns, is a collection of guidelines aimed at assigning responsibilities to classes and objects in object-oriented design. How can we use these patterns in our Javascript (Node.js) development? Of course, Javascript supports classes, which are natively built on prototypes, where we can apply GRASP similar way as we would do in Java.
However, in my opinion, GRASP patterns can also be applied to functional programming.
What is GRASP?
The nine GRASP patterns are:
- Information Expert
- Creator
- Controller
- Low Coupling
- High Cohesion
- Polymorphism
- Pure Fabrication
- Indirection
- Protected Variations
Information Expert
Assign responsibilities to functions that have the required data or knowledge to perform a task. In functional programming, this principle can be applied by assigning responsibilities to functions or modules that have the data or context required to perform a task.
// User management module
const createUser = (name, email) => ({ name, email });
const getUserEmail = (user) => user.email;
const updateUserEmail = (user, newEmail) => ({
...user,
email: newEmail,
});
const user = createUser('John Doe', 'john@example.com');
console.log(getUserEmail(user)); // 'john@example.com'
const updatedUser = updateUserEmail(user, 'john.doe@example.com');
console.log(getUserEmail(updatedUser)); // 'john.doe@example.com'
Creator
Use factory functions to create complex data structures or objects. In functional programming, while we donβt deal with classes in the same way as in object-oriented programming, we can apply the Creator principle by assigning the responsibility of creating data structures or initializing objects to functions or modules that have the necessary information and context.
const createUser = (name, email) => ({ name, email });
Controller
Use higher-order functions to handle system events and delegate tasks. In functional programming, controllers often take the form of functions that orchestrate the flow of data and actions between different parts of the system, ensuring that responsibilities are clearly separated.
// Example of express.js controller
const handleRequest = (req, res, userService) => {
const user = userService.createUser(req.body.name, req.body.email);
res.send(user);
};
Low Coupling
Ensure functions are independent and only depend on explicit inputs. In functional programming, low coupling is achieved by designing functions and modules that operate independently of each other, with minimal reliance on the internal details of other functions or modules
const sendEmail = (emailService, email) => emailService.send(email);
High Cohesion
High Cohesion refers to the degree to which the elements within a module or function belong together. In functional programming, achieving high cohesion means designing functions and modules so that they perform a single, well-defined task or closely related set of tasks.
const createUser = (name, email) => ({ name, email });
const addUser = (users, user) => [...users, user];
const createAndAddUser = (users, name, email)=>{
const user = createUser(name, email);
return addUser(users, user)
}
// usage
const users = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
];
const newUsers = createAndAddUser(users, 'Charlie', 'charlie@example.com');
Polymorphism
Use higher-order functions and first-class functions to achieve polymorphism. In functional programming, polymorphism is typically achieved through higher-order functions, generic functions, and type systems like Typescript
const processPayment = (paymentMethod) => paymentMethod.process();
Pure Fabrication
Create utility functions that do not directly correspond to domain concepts but provide necessary functionality, when no suitable domain function or class exists.
const log = (message) => console.log(message);
Indirection
Indirection in functional programming refers to the use of intermediate functions to manage interactions between different parts of a system. A good example in Node.js can be the Middleware Pattern.
const withNumberFilterMiddleware = (data) => data.filter(item => !isNaN(Number(item)));
Protected Variations
Protected Variations in functional programming mean creating a design that is resilient to changes by encapsulating the parts that vary and ensuring that the rest of the system is protected from these variations. In functional programming, this principle can be applied through the use of abstraction, immutability, and encapsulation to create robust and maintainable code that is less susceptible to changes.
const processCreditCardPayment = (amount) => {
console.log(`Processing credit card payment of ${amount}`);
// Credit card payment logic
};
const processPayPalPayment = (amount) => {
console.log(`Processing PayPal payment of ${amount}`);
// PayPal payment logic
};
const processPayment = (paymentMethod, amount) => {
paymentMethod(amount);
};
// Use different payment methods without changing the processPayment function
processPayment(processCreditCardPayment, 100);
processPayment(processPayPalPayment, 200);
Summary
As you can see GRASP principles are correlating with many known design patterns as well as SOLID principles. High Cohesion is almost equal to the Single Responsibility principle and so on.
Those principles are not only OOP principles but general principles for programming well-architected clean code, whether its functional or OOP programming.
Top comments (0)