Рассмотрим ещё одну простую систему — рациональные числа и операции над ними. Напомню, что рациональным называют число, которое может быть представлено в виде дроби 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 getDenomNumer = (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}`);
};
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →