Today, we have many good practices, principles (SOLID, DRY, KISS), GoF patterns, and more.
They are all trying to help us, developers, write good, clean, maintainable, and understandable code.
GRASP is an abbreviation of General Responsibility Assignment Software Patterns.
It’s a set of recommendations, principles, and patterns that are really good and could make our code much better. Let’s take a look at this list:
Today, we will learn the first 2 principles: Information expert and Creator.
Information expert might be the most important of all the GRASP patterns. This pattern says that all methods that work with data (variables, fields), should be in the same place where data (variables or fields) exist.
I know, I know, it doesn’t sound so clear, so let’s see an example. We want to create functionality that will calculate the total sum of our ordered items.
Let’s imagine that we have 4 files: main.js, OrderList, OrderItem, and Product.
The product could contain id, name, and price (and many other fields, that are not relative to our example):
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
OrderItem will be a simple object that contains product and count, like below:
class OrderItem {
constructor(product, count) {
this.product = product,
this.count = count
}
}
OrderList file will contain logic to work with an array of orderItems.
class OrderList {
constructor(items) {
this.items = items;
}
}
And, main.js is just that file that could contain some initial logic, can import OrderList, and do something with this list.
import { OrderItem } from './OrderItem';
import { OrderList } from './OrderList';
import { Product } from './Product';
const samsung = new Product('Samsung', 200);
const apple = new Product('Apple', 300);
const lg = new Product('Lg', 150);
const samsungOrder = new OrderItem(samsung, 2);
const appleOrder = new OrderItem(samsung, 3);
const lgOrder = new OrderItem(samsung, 4);
const orderList = new OrderList([samsungOrder, appleOrder, lgOrder]);
Where should the method that calculates the total sum be created? There are at least 2 files, and each of them we can use for this purpose, right? But which place will be better for our goal?
Let’s think about main.js.
We can write something like:
const totalSum = orderList.reduce((res, order) => {
return res + order.product.price * order.count
}, 0)
It will work. But, the orderItem file contains data without methods, the orderList file also contains data without method, and the main file contains a method that works with order items and order list.
It doesn’t sound good. If we want to add more logic that works with orders somehow, will we also put it in the main file? And, after some time, our main file will have a lot of different logic, for many thousands of code lines, which is really bad. This antipattern is called God object, where 1 file contains all.
How should it be if we want to use an information expert approach? Let’s try to repeat:
All methods that work with data (variables, fields), should be in the same place where data (variables or fields) exist.
This means: orderItem should contain logic that can calculate a sum for a specific item:
class OrderItem {
constructor(product, count) {
this.product = product,
this.count = count
}
getTotalPrice() {
return this.product.price * this.count;
}
}
And orderList should contain logic that can calculate a total sum for all order items:
class OrderList {
constructor(items) {
this.items = items;
}
getTotalPrice() {
return this.items.reduce((res, item) => {
return res + item.getTotalPrice();
}, 0);
}
}
And, our main file will be simple and won’t contain logic for that functionality; it will be as simple as possible (except for many imports, which we will remove soon).
So, any logic, that is relative to only one order item, should be placed to orderItem.
If something is relatively working with a set of orderItems, we should put that logic in orderItems.
Our main file should only be an entry point; do some preparation, and imports, and connect some logic with others.
This separation gives us a small number of dependencies between code components, and that is why our code is much more maintainable.
We can’t always use this principle in our project, but it’s a really good principle. And if you can use it, you should do it.
In our previous example, we had 4 files: Main, OrderList, OrderItem, and Product. Information expert says where methods should be: in the same place where data is.
But the question is: who and where should objects be created? Who will create orderList, who will create orderItem, who will create Product?
Creator says that each object (class) should be created only in the place where it will be used. Remember our example in the main file with many imports? Let’s check:
import { OrderItem } from './OrderItem';
import { OrderList } from './OrderList';
import { Product } from './Product';
const samsung = new Product('Samsung', 200);
const apple = new Product('Apple', 300);
const lg = new Product('Lg', 150);
const samsungOrder = new OrderItem(samsung, 2);
const appleOrder = new OrderItem(samsung, 3);
const lgOrder = new OrderItem(samsung, 4);
const orderList = new OrderList([samsungOrder, appleOrder, lgOrder]);
const totalSum = orderList.getTotalPrice();
As we can see, almost all imports and object creations are in main.js.
But, let’s think about who and where it’s really used.
The product is only used in OrderItem. OrderItem is only used in OrderList. OrderList is used on Main. It looks like this:
Main → OrderList → OrderItem → Prodcut
But if Main only uses OrderList, why do we create OrderItem in Main? Why do we also create a Product here? For this moment, our Main.js creates (and imports) almost everything. It’s bad.
Following the Creator principle, we should create objects only in the places where these objects are used. Imagine that, using our app, we added products to the cart. This is what it could look like:
Main.js: We create (and import) only OrderList here:
import { OrderList } from './OrderList';
const cartProducts = [{ name: 'Samsung', price: 200, count: 2 }, { name: 'Apple', price: 300, count: 3 }, {name: 'Lg', price: 150, count: 4 }];
const orderList = new OrderList(cartProducts);
const totalPrice = orderList.getTotalPrice();
OrderList.js: We create (and import) only OrderItem here:
import { OrderItem } from './OrderItem';
class OrderList {
constructor(items) {
this.items = items.map(item => new OrderItem(item));
}
getTotalPrice() {
return this.items.reduce((res, item) => {
return res + item.getPrice();
}, 0);
}
}
OrderItem.js: We create (and import) only Product here:
import { Product } from './Product';
class OrderItem {
constructor(item) {
this.product = new Product(item.name, item.price);
this.count = item.count;
}
}
Product.js:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
We have a simple dependency:
Main → OrderList → OrderItem → Product
And now, each object creates only in that place, where it is used. That’s what the Creator principle says.
I hope this introduction will be useful for you, and in the next series of GRASP, we will cover other principles.
Photo by Gabriel Heinzer on Unsplash