Уровневое проектирование

Рассмотрим ещё одну простую систему — рациональные числа и операции над ними. Напомню, что рациональным называют число, которое может быть представлено в виде дроби a/b, где a — это числитель дроби, b — знаменатель дроби. Причём b не должно быть нулём, поскольку деление на ноль не допускается.

Рациональные числа в JS не поддерживаются, поэтому абстракцию для них создадим самостоятельно. Как обычно, нам понадобятся конструктор и селекторы:

const num = makeRational(1, 2); // создали рациональное число "одна вторая"
const numer = getNumer(num); // 1
const denom = getDenom(num); // 2

С помощью трёх функций мы определили рациональное число. Одна функция (конструктор) собирает его из частей, другие (селекторы) позволяют каждую часть извлечь. Чем при этом, с точки зрения языка, является num — абсолютно не важно. Хоть функцией (и такое возможно), хоть массивом или объектом. Во внутренней реализации можно даже использовать строки:

const makeRational = (numer, denom) => `${numer}/${denom}`;

const getNumer = (rational) => rational.split('/')[0];

const getDenom = (rational) => rational.split('/')[1];

console.log(makeRational(10, 3)); // => 10/3

Несмотря на то, что мы научились представлять рациональные числа, эта абстракция сама по себе малоприменима. Абстракция становится полезна тогда, когда появляется возможность оперировать ей. Для рациональных чисел базовыми операциями можно считать арифметические, например, сложение, вычитание или умножение. Умножение рациональных чисел — самая простая операция. Для её выполнения нужно перемножить числители и знаменатели:

3/4 * 4/5 = (3 * 4)/(4 * 5) = 12/20

Самое интересное начинается в процессе реализации. Если предположить, что реальная структура рационального числа выглядит так: { numer: 2, denom: 3 }, то, чисто технически, решение может быть таким:

const mul = (rational1, rational2) => {
  return {
    numer: rational1['numer'] * rational2['numer'],
    denom: rational1['denom'] * rational2['denom']
  };
};

С точки зрения вызывающего кода всё нормально, абстракция сохранена. На вход в mul подаются рациональные числа, на выходе — рациональное число. А вот внутри никакой абстракции нет, обращение с рациональными числами строится на основе знания их устройства. Любое изменение внутренней реализации рациональных чисел потребует переписывания всех операций, работающих с рациональными числами напрямую — то есть без селекторов или конструктора. Данный код нарушает принцип одного уровня абстракции (single layer abstraction).

При разработке сложных систем используется подход — уровневое проектирование. Он заключается в том, что системе придаётся структура при помощи последовательных уровней. Каждый из уровней строится путём комбинации частей, которые на данном уровне рассматриваются как элементарные. Части, которые строятся на каждом уровне, работают как элементарные на следующем уровне.

Уровневое проектирование пронизывает всю технику построения сложных систем. Например, при проектировании компьютеров резисторы и транзисторы сочетаются (и описываются при помощи языка аналоговых схем), и из них строятся и-, или- элементы и им подобные, служащие основой языка цифровых схем. Из этих элементов строятся процессоры, шины и системы памяти, которые в свою очередь служат элементами в построении компьютеров при помощи языков, подходящих для описания компьютерной архитектуры. Компьютеры, сочетаясь, дают распределённые системы, которые описываются при помощи языков описания сетевых взаимодействий, и так далее. (c) SICP

const mul = (rational1, rational2) => {
  return makeRational(
    getNumer(rational1) * getNumer(rational2),
    getDenom(rational1) * getDenom(rational2)
  );
};

В нашем примере базовым уровнем являются типы, встроенные в сам язык: числа и объекты. На их основе сформирован уровень для представления рациональных чисел: makeRational, getDenom, getNumer. Затем — уровень, на котором реализованы арифметические операции над рациональными числами: сложение, вычитание, умножение и так далее.

Подчеркну, что речь идёт про реализацию самих уровней. Например, операция сложения полностью опирается на конструктор и селекторы, но ничего не знает и не может знать про внутреннее устройство самих рациональных чисел. С другой стороны, это не значит, что в одном месте не могут появиться функции из разных уровней. Могут и это нормально во многих случаях. Например:

const f = (rational1, rational2) => {
  const rational3 = sum(rational1, rational2);
  const denom = getDenom(rational3);
  const numer = getNumer(rational3);
  console.log(`Denom: ${denom}`);
  console.log(`Numer: ${numer}`);
};

Для полного доступа к курсу, нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам Хекслета, даст возможность обращаться за помощью к менторам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».