Что такое ГРАСП? Принципы информационного эксперта и создателя в JavaScript

Что такое ГРАСП? Принципы информационного эксперта и создателя в 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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE