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

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

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

Возьмем очень характерную черту многих веб-сайтов: покупки.

Типичный поток будет:

  1. Пользователь выбирает товары для покупки
  2. Пользователь предоставляет платежную информацию
  3. Система взимает плату с пользователя
  4. Система показывает пользователю подтверждение

У вас также будет очевидный «исключительный поток», когда их карта отклоняется:

  1. Система взимает плату с пользователя, но плата отклоняется
  2. Система показывает пользователю сообщение об ошибке
  3. Пользователь обновляет платежную информацию и пытается снова

Оба этих потока являются «счастливым путем» и могут быть наивно реализованы с использованием простого синхронного кода:

class PurchasesController
  def create
    customer = current_user
    order    = customer.orders.find(params[:order_id])
    result = MegaPayments.create_txn(id: customer.id,
                                     order_number: order.id,
                                     amount: order.total.to_s)
    if result.declined?
      flash[:error] = "There was a problem charging your card, please update your billing information"
      render :new
    else
      redirect_to orders_path(order)
    end
  end
end

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

Из-за нашего наивного дизайна продукта и соответствующей реализации мы создаем ситуацию, когда наши клиенты могут завышать цену.

Перед написанием строчки кода нам нужно было спросить: «Что делать, если обработчик платежей работает медленно или не отвечает? Каким должен быть пользовательский опыт в таком случае? "

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

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

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

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

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

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

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