Этот пост изначально был размещен на kellysutton.com. Это сообщение было изменено, чтобы соответствовать вашему экрану.

В Gusto мы по колено провели существенный рефакторинг нашей системы для работы с платежными ведомостями.

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

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

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

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

Серверный код Gusto написан на Ruby, языке, который обычно известен своими объектно-ориентированными корнями и корнями в метапрограммировании. Тем не менее, мы хотели интегрировать в наш код еще несколько функциональных концепций в надежде повысить безопасность и ясность системы. В результате получился поддерживаемый код, который легче рассуждать и его безопаснее изменять.

Обнимая PFaaO

Ruby - выразительный язык, но он не поддается некоторым общим функциональным практикам. Хотя Ruby допускает замыкания и функции первого класса через Procs, в идиоматическом Ruby не видно, чтобы многие Procs передавались как объекты.

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

Для этого мы использовали паттерн Чистая функция как объект (PFaaO). По сути, вы проектируете объекты как чистую функцию, но одеваете их как классы Ruby.

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

Простой PFaaO может выглядеть следующим образом:

class PayrollCalculator 
  def self.calculate(payroll) 
    new(payroll).calculate 
  end 
  def initialize(payroll) 
    @payroll = payroll 
  end   
  private_class_method :new 
  def calculate 
    PayrollResult.new( 
      payroll: payroll, 
      paystubs: paystubs, 
      taxes: taxes, 
      debits: debits 
    ) 
  end 
  def paystubs 
    # ... 
  end 
  def taxes 
    # ... 
  end 
  def debits 
    # ... 
  end 
end

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

Во-первых, у нашего класса есть только один эффективный открытый интерфейс: PayrollCalculator.calculate. Поскольку мы объявили конструктор закрытым с помощью private_class_method :new, метод экземпляра #calculate фактически является закрытым.

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

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

Ссылочная прозрачность бесплатно

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

Теперь, поскольку каждый из этих частных методов является чистой функцией, у нас есть ссылочная прозрачность. Это означает, что мы можем заменить метод и его параметры возвращаемым значением. Думайте об этом как об алгебре: учитывая функцию f(x) = x + 5, вы можете безопасно заменить любое вхождение f(2) значением 7.

Что это значит для рубистов? Бесплатная и безопасная памятка:

def paystubs 
  calculate_paystubs(taxes, ...) 
end 
def debits 
  calculate_debits(taxes, ...) 
end 
def taxes 
  @taxes ||= calculate_taxes(@payroll) 
end

Мемоизация - это форма кэширования, и она может быть сопряжена с проблемами, если мемоизированное значение на самом деле не исходит от чистой функции. Но поскольку мы делаем все в PFaaO чистым, мы можем безопасно запоминать этот вызов метода.

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

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

Расширение PFaaOs

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

PFaaO в Ruby хороши тем, что их легко поддерживать. Их не только легко проверить, но и они предрасположены к здоровому росту.

Что я имею в виду?

Давайте снова возьмем пример нашего #taxes метода. В начале истории Gusto (когда он еще назывался ZenPayroll) мы предлагали услуги по расчету заработной платы только в Калифорнии. Таким образом, нам нужно было беспокоиться только о налогах на заработную плату в Калифорнии.

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

def taxes 
  federal_taxes(@payroll) + 
    california_taxes(@payroll) + 
    local_taxes(@payroll) 
end

Теперь предположим, что мы расширились до нового штата, Нью-Йорка. Теперь наш метод немного разросся:

def taxes 
  federal_taxes(@payroll) + 
    california_taxes(@payroll) + 
    new_york_taxes(@payroll) + 
    local_california_taxes(@payroll) +
    local_new_york_taxes(@payroll) 
end

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

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

def taxes 
  PayrollCalculator::Taxes.calculate(@payroll) 
end

По мере того как мы разбираем эти разные PFaaO, мы также получаем гораздо лучшее представление о входных требованиях для этих классов обслуживания. Наш @payroll - это большой объект-параметр, и каждому извлеченному PFaaO, вероятно, требуется только подмножество его данных.

Таким образом, нам может сойти с рук что-то вроде:

def taxes 
  PayrollCalculator::Taxes.calculate(
    @payroll.only_pay_and_location_data 
  ) 
end

Здесь мы предполагаем, что Payroll#only_pay_and_location_data возвращает часть общих данных в экземпляре как новый объект значения. Этот объект значения представляет только данные, необходимые для расчета налоговой части расчета заработной платы.

Данные по умолчанию неизменяемы

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

Каждый раз, когда вы берете свой =, вам нужно заменять его на #set или #put. Вместо того, чтобы изменять объекты на месте, вы привыкнете возвращать новые копии с новыми значениями. (Hamster, который предоставляет отличные неизменяемые структуры данных, может помочь вам избавиться от необходимости вручную настраивать функциональность FP.)

Что это значит для Rails? Часто это означает создание функций или классов, которые принимают ActiveRecord объекты и преобразуют их в объекты неизменяемых значений. Для нас мы выделяем эти объекты значений в пространство имен того, что мы делаем. Например, вот два представления платежной ведомости в нашей системе:

# app/models/payroll.rb 
class Payroll < ActiveRecord::Base 
end 
# app/services/payroll_calculator/payroll.rb 
class PayrollCalculator::Payroll < ValueObject 
end

ActiveRecord версия платежной ведомости представляет данные, которые хранятся в базе данных. Это расширенный набор данных, необходимых для фактического расчета заработной платы. Хотя у них одинаковое имя, у них разные атрибуты. Например, ActiveRecord версия Payroll будет иметь атрибут processed_at, тогда как Payroll, который находится в домене вычислений, не имеет.

Говоря словами Domain-Driven Design, каждое пространство имен здесь представляет собой отдельный ограниченный контекст. Мы внедряем адаптеры, чтобы принимать ActiveRecord платежные ведомости и превращать их в PayrollCalculator платежные ведомости, и наоборот.

Положительные стороны этого явления можно увидеть в любой другой большой системе с четко определенными абстракциями; изменения в моделях не влияют на домены. В нашем примере мы можем изменить структуру Payroll в нашей базе данных без изменения кода расчета. Нам нужно будет только сменить наш адаптер. Более того, этот контекст полностью отделен от махинаций Rails. Мы могли бы легко и безопасно поместить это в отдельный драгоценный камень или полностью отдельный сервис.

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

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

Заключение

Мы постепенно перестраиваем наши калькуляторы заработной платы в сторону этой модели и используем ее для безопасной обработки более 1 миллиарда долларов в месяц.

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

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

Однако это не все солнечные лучи и радуги. Такой подход действительно приводит к увеличению объема кода. По моим приблизительным оценкам, объем кода увеличится примерно в 1,5–2 раза. Некоторым разработчикам не нравится разрастающийся характер множества возникающих в результате PFaaO. Хотя общее количество строк кода будет увеличиваться, этот подход должен помочь вашей команде лучше понять требования к данным для каждого ограниченного контекста. Другими словами: вам не нужно передавать целые ActiveRecord объекты, а только небольшие группы их атрибутов.

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

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

Для нас мы применяем его везде, где это уместно. Попробуйте этот узор и дайте мне знать, как он работает!

Особая благодарность Джастину Дьюку, Эдди Киму, Бо Соренсену, Мэтту Льюису и Джулии Ли за отзывы о ранних черновиках этого поста.

Первоначально опубликовано на сайте engineering.gusto.com 2 октября 2017 г.