В докладе Границы Гери Бернхардт рассказывает про типы в качестве границ приложения. А в Type-Driven Design in Swift Alex Ozun показывает конкретную реализацию через паттерны.

Для реализации этих идей на TypeScript есть библиотека True Myth.

Из их документации:

Quote

True Myth предоставляет стандартные, типобезопасные оболочки и вспомогательные функции, которые помогут вам справиться с двумя чрезвычайно распространенными случаями программирования:

  • отсутствие значения;
  • наличие значения result, в котором имеется дело либо с успехом, либо с неудачей.

Разработчики назвали библиотеку в дань уважения идеи, которую высказал Толкин о мифах.

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

Quote

Любая достаточно развитая технология неотличима от магии

Авторизация в Мории

В Морию через врата Дурина могут попасть только друзья. Гномы первой эпохи не особо запаривались над безопасностью, поэтому для прохода нужно просто сказать на эльфийском слово “друг”.

Я спроектировал тип странника так, чтобы он был либо другом, либо чужаком:

type Traveller = Stranger | Friend;

где Stranger это пустой класс, потому что к нему нет пока никаких требований, им может быть любой:

class Stranger {}

а тип Friend нельзя создать без передачи в него корректного кода

export class Friend {
  code: CorrectCode;
 
  constructor(params: Friend) {
    this.code = params.code;
  }
}

Убирая ts-синтаксис и оставив только логику можно сказать:

Friend = CorrectCode

Что собой представляет класс верного кода? В докладе Озуна такой класс называется Parsed Wrapper. Его нельзя создать, если не передана корректная строка.

Для этого нужно:

  1. Сделать приватным конструктор
  2. Реализовать статический метод, который по нашим условиям либо возвращает результат c экземпляром друга: Result.ok(CorrectCode), либо ошибку: Result.err(AppError)

Result

import Result, { ok, err } from 'true-myth/result';
 
export class CorrectCode {
  public code: Mellon;
 
  static parse(code: string): Result<CorrectCode, AppError> {
    if (code === 'mellon') {
      return ok(new CorrectCode(code));
    } else {
      return err(CodeError.InvalidСode);
    }
  }
 
  private constructor(code: Mellon) {
    this.code = code;
  }
}
  • ok(new CorrectCode(code)) - вернёт контейнер Result с валидным объектом кода;
  • err(error) - вернёт контейнер Result с ошибкой;

Но в обоих случаях вернётся Result, который дальше можно обработать.

Убирая синтаксическую мишуру:

CorrectCode = 'mellon'

Получается, что без корректной строки не создать тип валидного кода, а без него не создать друга, без друга не попасть в Морию:

'mellon' -> CorrectCode -> Friend -> entrance

Обработка Result и создание инстанса друга:

  const codeResult = CorrectCode.parse(code);
 
  const traveller: Traveller = codeResult.isOk
    ? new Friend({ code: codeResult.value })
    : new Stranger();

Границы выставлены достаточно чётко и можно удобно работать отдельно над каждой из них, уточняя детали и меняя правила.

Предположим, что у гномов в Мории появился продукт овнер и вот он, попивая смузи, говорит:

Quote

Ребят, всё конечно хорошо, но почему мэллон можно сказать только на латинице? Где вторая по популярности в интернете система письма?

Это легко поправить, заменив условие внутри класса CorrectCode:

if (code === 'mellon') {
 return ok(new CorrectCode(code));
}

на другое:

switch (code) {
	case 'mellon':
	case 'мэллон':
	  return ok(new CorrectCode(code));
}

unit-тест будет выглядеть так:

test('На кириллице', () => {
	const codeResult = CorrectCode.parse('мэллон');
 
    expect(codeResult.isOk).toBe(true);
	expect(
		codeResult.isOk && codeResult.value
	).toBeInstanceOf(CorrectCode);
});

Снова залетает гном-продукт овнер:

Quote

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

И мы понимаем, что формально Friend уже не просто CorrectCode, теперь он:

Friend = CorrectCode & Name

Но имя это не любая строка. К имени тоже есть требования. Описать их можно, создав уже знакомый ParsedWrapper:

import Result, { ok, err } from 'true-myth/result';
 
export class Name {
  public string: string;
 
  static parse(rawName: string): Result<Name, AppError> {
    if (rawName.length === 0) return err(NameError.TooShort);
    if (rawName.length > 20) return err(NameError.TooLong);
 
    return ok(new Name(rawName));
  }
 
  private constructor(string: string) {
    this.string = string;
  }
}

Теперь нужно добавить его как правило для создания друга:

export class Friend {
  code: CorrectCode;
  name: Name;
 
  constructor(params: Friend) {
    this.code = params.code;
    this.name = params.name;
  }
}

В хэндлер входа добавляются соответственно строки с именем:

const codeResult = CorrectCode.parse(event.code);
const nameResult = Name.parse(event.name);
 
const traveller: Traveller =
codeResult.isOk && nameResult.isOk
  ? new Friend({ code: codeResult.value, name: nameResult.value })
  : new Stranger();

Теперь мы можем гарантировать, что если мы где-то в приложении встретили инстанс друга, то у него точно есть код и имя, причём с валидными данными ещё и этих инстансов.

У Озуна в докладе есть про это отличная красно-зелёная картинка с фазами.

Это можно использовать, создавая приветствие. В презентаторе страницы приветствия следующий код:

export const welcomePresentation = (
  friend: Friend
): WelcomeViewState =>
  new WelcomeViewState({
    greeting: `Wellcome ${friend.name.string}`,
  });

Обратите внимание, что в презентатор можно передать только друга, то есть его не создать без правильного инстанса.

Внедрение контейнера Result позволяет повысить надёжность приложения и снять лишнюю когнитивную нагрузку. Валидации в логике больше нет.

Maybe

На этот раз мы общаемся с гномьим дизайнером, который, под эльфийским модным влиянием, набросал леттеринг, что должен светится только при звёздном и лунном свете, но скрываться при солнечном. Wft?

Техлид начинает умничать, что луна отражает свет солнца, а солнце это тоже звезда и то, что нельзя так просто взять и… В конце концов действуют рестрикции на экспорт магии из кап. стран.

Но дизайнер кидает ссылку на лукошко-экспериментального-энтузиазма.io с прототипом, где с помощью спектрометра, и не очень глубокого анализа можно понять, что сейчас светит на стену.

И вот уже крутится сервис, который может прислать данные с аналитикой, что за небесное тело сейчас освещает стену. Нам нужно только, оперируя абстракциями, принимать решения.

Контракт такой:

type Light = Sunlight | Starlight | Moonlight
 
type LightDto = Light | null

То есть мы можем по типу понять, что сейчас светит за объект, если объект не классифицирован или свет недостаточен для анализа, придёт null.

Для таких ситуаций, когда значение может быть, а может и нет, разработан контейнер Maybe.

Он позволяет обернуть возможное значение. И если оно есть, то обработчики, будут запущены.

import { of } from 'true-myth/maybe';
 
const isIthildinShining = of(light).map(handleLight).unwrapOr(false)
  • of(light) - возвращает объект типа Maybe;
  • map(handleLight) - безопасно запускает функцию handleLight;
  • unwrapOr(false) - извлекает значение из контейнера, если значение не валидное, то вернёт false.

Это очень похоже на работу функции map в js. Только в массивах она не запускается, если массив пустой, а тут, если значение null.

Круто то, что handleLight вообще не знает о том, что light может не прийти, его опциональность не прорастает в код:

export const handleLight = (light: Light): boolean => {
  switch (light.constructor) {
    case Moonlight:
    case Starlight:
      return true;
    default:
      return false;
  }
};

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

Мысли и комментарии

  • Maybe и Result - это классические специальные монады из функционального программирования. Их можно встретить под похожими или такими же именами в фп книжках;

  • map - это функтор. Тоже классическая штука из мира фп. В true-myth предоставлено ещё множество других со своими задачами;

  • в книге “Функциональное программирование на JavaScript” в главе 5 приведена своя реализация Result и Maybe. С методами, которые пересекаются с теми, что есть в библиотеке. Они настолько простые, что, как говорят сами создатели: “их может реализовать любой. Но зачем это делать, когда есть готовое протестированное решение?”;

  • @tolstoymv скинул библиотеку с функциональными конструкциями для ts, которую использует он. Прикрепляю в качестве альтернативы;

  • описанные контейнеры имеют гораздо больше возможностей, чем я показал. С полным списком методов рекомендую ознакомиться в документации;

  • честно говоря, пока опасаюсь добавлять в рабочий код все конструкции, но например отделение AnonymousUser от SignedInUser уже реализовано и отлично работает по тому же принципу в Ozon Acquiring;

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

  • но всё же, любой джун повсеместно использует конструкции типа:

    • response.then((data) => { ... })
    • list.map(item => { ... })
    • arr.reduce(reducer)

Вдохновляет

  • То, что “Властелин Колец”, “Хоббит”, “Сильмариллион” и др. книги Толкина это побочный продукт от главного - создания языков. Любой язык это отражение производственных отношений, он не отделим от материального базиса. Толкин это прекрасно понимал и написал историю мира, в котором развивались его языки;

  • Отношение Питера Джексона к коллективной работе и творческому процессу. Вместо того, чтобы снять своё видение Средиземья, он обратился к фанбазе толкинистов, у которой уже было огромное множество наработок по тому, как должно всё выглядеть, звучать и работать. Именно поэтому получилось создать такое цельное и глубокое кино. Кстати, в титрах указали вообще всех причастных. Они идут почти 30 минут, Карл;

  • Идея сделать для разрабов то, что делают Awdee для дизайнеров.

Материалы

Ссылки

Тэги

typeDrivenDesign,functionalProgramming,result,maybe,монады,функторы,ddd