По мере роста вашей организации, занимающейся разработкой программного обеспечения, становится все труднее координировать работу.

Позвольте мне проиллюстрировать это. Допустим, вы работаете над новым рабочим процессом, который требует от вас отправки push-уведомлений некоторым клиентам вашего мобильного приложения. Другая команда поддерживает OutboundMessagingService, микросервис, написанный на Java, который занимается всеми нюансами отправки push-уведомлений пользователям на мобильных платформах, таких как iOS и Android. Вы, как надежный инженер-программист, хотите повторно использовать их хорошую работу, что вы делаете?

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

Вы уточняете у них, и они действительно поддерживают клиентскую библиотеку для своих проектов, но, к сожалению для вас, они написали ее на Java, основном языке, на котором работает их команда. Нет планов публиковать клиент Ruby, который вам действительно нужен. это приложение Rails, над которым вы работаете. Тем не менее, они столкнулись с проблемой документирования API. Один из инженеров в команде отправляет вам ссылку на вики-страницу компании с документацией, и вы начинаете создавать своего клиента.

На полпути к написанию кода вашего Ruby-клиента вы выполняете небольшой тестовый запуск и пытаетесь сделать фактический вызов службы, как указано в документации:

{
  "userId": "9C96A22B-A4F1-4965-BE4C-64C51A1CF519",
  "message": "Hello, world!"
}

Независимо от того, как вы изменяете URL-адрес запроса, воссоздаете токен аутентификации, вы продолжаете получать этот загадочный ответ с кодом состояния HTTP 400:

{
  "error": "Bad Request"
}

Вы уточняете у одного из инженеров в команде. Вместе вы начинаете просматривать OutboundMessagingService журналы, пока не найдете свой запрос и не найдете исключение. «О! - говорит инженер, с которым вы работаете, - несколько месяцев назад мы заменили поле message объектом payload. Мы обновили клиент Java, но, думаю, никто не забыл обновить документацию в Wiki, извините ».

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

В этом случае вам посчастливилось попасть в команду, которая все еще обслуживала сервис, и они достаточно быстро отреагировали, чтобы решить проблему вместе с вами. Если бы это было не так, вам, вероятно, пришлось бы копаться в исходном коде их службы, чтобы попытаться выяснить, что к чему. В одном крайнем случае, который случился со мной, мне пришлось установить wirehark на серверную машину, чтобы отслеживать реальный трафик, чтобы выяснить, как внедрить клиента в один из наших сервисов на работе. У меня был исходный код на языке, который я плохо читал, чтобы направлять меня, представьте, что вы делаете это против скомпилированной службы черного ящика!

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

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

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

Статически типизированные языки

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

Во-первых, давайте начнем с некоторых определений, чтобы мы находимся на одной странице. Когда я говорю о статически типизированном языке, я имею в виду язык (и компилятор / интерпретатор), в котором типы значений известны до выполнения и проверяются во время компиляции. Напротив, Python, язык с динамической типизацией (и интерпретатор), не заботится о типах до тех пор, пока функция не будет фактически вызвана:

def add(a, b):
  return a + b
# this works
add("a", "b") # returns string "ab"
# and this also works
add(1, 2) # returns int 3

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

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

def add(a, b):
  assert type(a) == type(b) == int, 'a and b must be integers'
  return a + b
add(1, "b") # fails with : AssertionError: a and b must be integers

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

В языке со статической типизацией типы переменных известны до выполнения и проверяются во время компиляции. Это, конечно, не происходит волшебным образом, как показывает практика, в статически типизированных языках код будет более подробным: мы явно определим сигнатуры наших функций. Но такое многословие дает преимущества типобезопасности, например, в Go:

func add(a, b int) int {
  return a + b
}
add("a", "b") // will not compile: cannot use "a" (type string) as type int in argument to add
add(1, 2) // will compile and return 3

Под «безопасностью типов» я подразумеваю, что в приведенном выше примере компилятор Go не позволит нам делать ошибки типа, передавая функции неправильный тип.

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

(наша IDE знает, чтоcfg.Redis.Tier1 имеет типRedisConfig и, следовательно, имеет поляPort, ReadEndpoint и []WriteEndpoints)

А также предупредить нас, когда мы введем что-то, что не скомпилируется:

(наша IDE знает, чтоAdd() должна возвращать anint, и поэтому предупреждает нас, что наш код не скомпилируется)

Теперь, когда все определения определены, я даю вам:

Статически типизированная организация

В организации со статической типизацией:

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

Преимущества работы в статически типизированной организации (далее STO):

  • Легче кодировать. Разработчики освобождаются от коммуникационных и умственных затрат, связанных с попытками выяснить схему сообщений, которые они должны передавать. Гадание на поиск того, что может вернуть служба (имя поля, типы полей), исключено, что приводит к гораздо более высокой скорости разработки и меньшему количеству ошибок типа "картофель-картофель".
  • Межгрупповая безопасность типов. Разработчики, работающие со статически типизированными языками, получают полное завершение кода и проверку типов для всех API организации по мере ввода; те, кто работает с динамическими языками, по крайней мере, получают исключения времени выполнения очень рано в своем рабочем цикле. Более быстрая обратная связь способствует дальнейшему увеличению скорости разработки.
  • Проще запустить проект. Интерфейсы между службами определяются до реализации, то есть клиенту не нужно ждать, пока сервер будет реализован, чтобы начать работу. Моки сервера могут быть сгенерированы программно для начальной загрузки функции. Кроме того, клиентский код в его окончательной форме создается с самого начала, что приводит к еще более высокой скорости разработки.
  • Использование существующих механизмов контроля качества. Если схемы контролируются версиями, в общем репозитории, изменения схемы подлежат обычным процессам проверки кода и непрерывной интеграции, способствующим соблюдению организационных стандартов в отношении именования и общих качество кода.
  • Более эффективные форматы проводов. Поскольку сериализаторы и десериализаторы создаются программно и являются общими для всей организации, их можно оптимизировать один раз для каждого языка. Используя специализированные кодировки, можно добиться экономии места / времени работы, которую отдельный разработчик, работающий над реализацией функции продукта, редко будет иметь время и ресурсы, чтобы внести свой вклад в более эффективное использование ресурсов (ЦП, память, пропускная способность сети).

Стоимость работы в СТО:

  • Больше процесса. Изменение подписи общедоступной функции или добавление поля к сообщению происходит медленнее: разработчику необходимо сделать PR репозитория схемы, а также репозитория приложения.
  • Постоянная регенерация классов схемы. Каждое изменение схемы требует перекомпоновки сгенерированных классов.
  • Работа с сгенерированным кодом. Сгенерированный код нельзя редактировать (обычно не следует возвращать в систему управления версиями). Поэтому классы, как правило, будут исключительно контейнерами данных, добавление бизнес-логики к классам сущностей потребует их некоторого расширения (посредством наследования / встраивания). Еще одна проблема заключается в том, что обычно сгенерированный машиной код уродлив, труден для чтения, и любые обнаруженные в нем ошибки не могут быть исправлены напрямую - вместо этого необходимо исправлять генератор кода.
  • Разработчик на накладных расходах. Новых сотрудников необходимо обучить понимать и использовать эти рабочие процессы.

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

Размышляя о том, чтобы стать STO, мы также должны учитывать стоимость отсутствия формально определенных программно реализованных схем, а именно:

  • Больше ошибок. Высокая вероятность появления ошибок, связанных со схемой, увеличивается с увеличением размера организации.
  • Потраченное время на локальную оптимизацию. Опытные инженеры, работающие в небезопасной среде, приложат все усилия, чтобы защитить свои программы от таких ошибок, внедряя специальную проверку типов во время выполнения и разрабатывая большие наборы тестов. которые пытаются смоделировать любые подобные ошибки, которые они могут вообразить. Это, конечно, расточительная локальная оптимизация, которая будет повторяться снова и снова, если не будет применен какой-либо другой инфраструктурный подход.

О следующем выпуске «Статистически типизированной организации»:

На этом первая часть этой серии завершается, в следующих частях мы обсудим:

  • Рекомендации по сериализации и десериализации сообщений в STO
  • Буферы протокола как технология для реализации STO
  • Дополнительные рекомендации и уроки, извлеченные из STO

Первоначально опубликовано в моем блоге по разработке программного обеспечения placeholder