Pipelining & Composing: улучшаем читаемость кода. Реализация на TypeScript

в 9:42, , рубрики: compose, function, javascript, pipeline, TypeScript

Как часто вам приходилось видеть что-то подобное в коде?

const result = fnD(fnC(fnB(fnA(...))));
                   

Чтобы получить результат, нужно последовательно выполнить каждую функцию, начиная с самой внутренней. Это требует визуального "разворачивания" функций, что усложняет понимание логики кода. Когда мы сталкиваемся с таким кодом, то сразу осознаем, что его чтение и поддержка могут стать настоящим испытанием.

В этой статье мы рассмотрим, как можно значительно улучшить читаемость кода с помощью двух мощных техник: пайплайнинга (pipelining) и композиции функций (composing). Мы будем использовать TypeScript для демонстрации этих подходов.

Начнем с условного примера, который иллюстрирует, как вложенные вызовы могут затруднить восприятие кода:

const add = (a: number, b: number) => a + b;
const square = (x: number) => x * x;
const half = (x: number) => x / 2;
const result = half(square(add(2, 2))); // 8

В приведенном примере функции вложены друг в друга, что может быстро привести к сложностям в управлении кодом. Каждый новый шаг в обработке данных усложняет восприятие результата и последовательности функций.

Pipelining

Pipelining — это техника передачи данных через цепочку операций, имеющая корни в математической теории. Результат одной функции передаётся следующей функции, формируя непрерывный поток обработки данных.

Реализация c использованием замыкания:

const pipeline = <FNS extends FN[]>(...fns: FNS) => {
  return (...args: FNS) => {
	let result = fns[0](...args);
	for (let i = 1; i < fns.length; i++) {
  		result = fns[i](result);
	}
	return result;
  };
};
pipeline(
	add, 
	square, 
	half
)(2, 2); // 8

Как мы видим, использование pipeline() облегчает читаемость кода, т.к. создает линейный поток данных и благодаря этому, устраняет сложную вложенность вызовов функций.

Напишем pipeline() с использованием метода reduce():

const pipeline = <FNS extends FN[]>(...fns: FNS) => {
  return fns.reduce(
	(prevFn: FN, nextFn: FN) =>
  	(...args: Parameters<FNS[0]>) =>
    	nextFn(prevFn(...args))
  );
};

Применение метода reduce() позволяет написать более элегантную реализацию pipeline().

Composing

Composing, как и pipelining, имеет корни в математической теории.
Это последовательность вызовов функций, в которой выходные данные одной функции становятся входными данными для следующей функции, но в порядке, противоположном тому, который используется в pipelining.

Реализация c использованием замыкания:

const compose = <FNS extends FN[]>(...fns: FNS) => {
  return (...args: FNS) => {
	let result = fns[fns.length - 1](...args);
	for (let i = fns.length - 2; i >= 0; i--) {
  		result = fns[i](result);
	}
	return result;
  };
};

Реализация с использованием pipeline():

const compose = <FNS extends FN[]>(...fns: FNS) => {
	return pipeline(...fns.reverse());
};
compose(
	half, 
	square, 
	add
)(2, 2);  // 8

Заключение:

В статье мы рассмотрели две техники — pipelining и composing.

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

В pipeline() данные проходят через функции последовательно — от первой к последней,
а в compose() функции применяются в обратном порядке — от последней к первой.

Оба подхода способствуют созданию более ясной структуры кода, что значительно улучшает его читаемость.

Автор: Serj_Pashnin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js