Что такое ГРАСП? Принципы информационного эксперта и создателя в JavaScript
8 июня 2022 г.Сегодня у нас есть много хороших практик, принципов (SOLID, DRY, KISS), шаблонов GoF и многого другого.
Все они пытаются помочь нам, разработчикам, писать хороший, чистый, поддерживаемый и понятный код.
GRASP – это сокращение от Шаблоны программного обеспечения для назначения общей ответственности
.Это набор рекомендаций, принципов и шаблонов, которые действительно хороши и могут сделать наш код намного лучше. Давайте посмотрим на этот список:
* Информационный эксперт * Создатель * Контроллер * Низкая связь * Высокая сплоченность * Чистое изготовление * Косвенность * Защищенные варианты * Полиморфизм
Сегодня мы изучим первые два принципа: информационный эксперт и создатель.
Эксперт по информации
Эксперт по информации может быть самым важным из всех шаблонов GRASP. Этот шаблон гласит, что все методы, работающие с данными (переменными, полями), должны находиться в том же месте, где существуют данные (переменные или поля).
Я знаю, я знаю, это звучит не так ясно, поэтому давайте посмотрим на пример. Мы хотим создать функцию, которая будет подсчитывать общую сумму заказанных товаров.
Представим, что у нас есть 4 файла: main.js, OrderList, OrderItem и Product.
Товар может содержать идентификатор, название и цену (и многие другие поля, не относящиеся к нашему примеру):
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
OrderItem будет простым объектом, содержащим продукт и количество, как показано ниже:
class OrderItem {
constructor(product, count) {
this.product = product,
this.count = count
}
}
Файл OrderList будет содержать логику для работы с массивом элементов порядка.
class OrderList {
constructor(items) {
this.items = items;
}
}
А main.js — это как раз тот файл, который может содержать некоторую начальную логику, может импортировать OrderList и что-то делать с этим списком.
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]);
Где должен быть создан метод, вычисляющий общую сумму? Есть как минимум 2 файла, и каждый из них мы можем использовать для этой цели, верно? Но какое место будет лучше для нашей цели?
Давайте подумаем о main.js.
Мы можем написать что-то вроде:
const totalSum = orderList.reduce((res, order) => {
return res + order.product.price * order.count
}, 0)
Это будет работать. Но файл orderItem содержит данные без методов, файл orderList также содержит данные без методов, а основной файл содержит метод, который работает с элементами заказа и списком заказов.
Звучит не очень хорошо. Если мы захотим добавить больше логики, которая как-то работает с ордерами, мы ее тоже поместим в основной файл? И, через какое-то время, в нашем основном файле будет много разной логики, на многие тысячи строк кода, что совсем плохо. Этот антипаттерн называется ==God object==, где 1 файл содержит все.
Как должно быть, если мы хотим использовать подход информационного эксперта? Попробуем повторить:
Все методы, работающие с данными (переменными, полями), должны находиться там же, где существуют данные (переменные или поля).
Это означает: orderItem должен содержать логику, которая может вычислить сумму для определенного элемента:
class OrderItem {
constructor(product, count) {
this.product = product,
this.count = count
}
getTotalPrice() {
return this.product.price * this.count;
}
}
И orderList должен содержать логику, которая может вычислить общую сумму для всех позиций заказа:
class OrderList {
constructor(items) {
this.items = items;
}
getTotalPrice() {
return this.items.reduce((res, item) => {
return res + item.getTotalPrice();
}, 0);
}
}
И наш основной файл будет простым и не будет содержать логики для этой функциональности; это будет максимально просто (за исключением многих импортов, которые мы скоро удалим).
Таким образом, любая логика, относящаяся только к одному элементу заказа, должна быть помещена в orderItem.
Если что-то относительно работает с набором orderItems, мы должны поместить эту логику в orderItems.
Наш основной файл должен быть только точкой входа; сделайте некоторую подготовку, импортируйте и соедините одну логику с другой.
Это разделение дает нам небольшое количество зависимостей между компонентами кода, и поэтому наш код намного удобнее в сопровождении.
Мы не всегда можем использовать этот принцип в нашем проекте, но это действительно хороший принцип. И если вы можете это использовать, вы должны это сделать.
Создатель
В нашем предыдущем примере у нас было 4 файла: Main, OrderList, OrderItem и Product. Информационный эксперт говорит, где должны быть методы: там же, где и данные.
Но вот вопрос: кто и где должен создавать объекты? Кто создаст orderList, кто создаст orderItem, кто создаст Product?
Создатель говорит, что каждый объект (класс) должен создаваться только в том месте, где он будет использоваться. Помните наш пример в основном файле с большим количеством импортов? Давайте проверим:
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();
Как мы видим, почти весь импорт и создание объектов находятся в main.js.
Но давайте подумаем, кто и где его реально использует.
Продукт используется только в OrderItem. OrderItem используется только в OrderList. OrderList используется на Main. Это выглядит так:
Главная → OrderList → OrderItem → Prodcut
Но если Main использует только OrderList, зачем мы создаем OrderItem в Main? Почему мы также создаем Продукт здесь? На данный момент наш Main.js создает (и импортирует) почти все. Это плохо.
Следуя принципу ==Creator==, мы должны создавать объекты только в тех местах, где эти объекты используются. Представьте, что с помощью нашего приложения мы добавили товары в корзину. Вот как это может выглядеть:
Main.js: Здесь мы создаем (и импортируем) только OrderList:
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: здесь мы создаем (и импортируем) только OrderItem:
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: Мы создаем (и импортируем) только Product здесь:
import { Product } from './Product';
class OrderItem {
constructor(item) {
this.product = new Product(item.name, item.price);
this.count = item.count;
}
}
Продукт.js:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
У нас есть простая зависимость:
Главная → Список заказов → Элемент заказа → Товар
И теперь каждый объект создается только в том месте, где он используется. Именно об этом говорит принцип Creator.
Я надеюсь, что это введение будет для вас полезным, и в следующей серии GRASP мы рассмотрим другие принципы.
Фото Габриэля Хайнцера на Unsplash
Оригинал