Държава срещу Актьор

Въведение

Когато започнах да използвам Akka, беше общ подход за поддържане на състояние на актьор с бизнес логика вътре. По-късно открих недостатъците на този подход, трудно е да се използва повторно логиката, да се добави нова функционалност, тестването изисква използване на Akka и тествам странични ефекти. За да реша тези проблеми се опитах да използвам държавна монада. Тази публикация в блога описва процеса на рефакторинг.

Бизнес домейн

Този примерен проект (връзка към github) е проста реализация на автомат, използващ Akka. Симулира автомат с програма, която комуникира с потребителя чрез терминал. Потребителят може да инициира стандартни действия като вмъкване на монета (+1, +5,…), избор на продукт (1, 2,…) или теглене на пари (-).

Актьорът на автомата има следните функции:

  • проследяващ размер на кредита
  • проследявайте количеството и срока на годност на продуктите
  • продават продукт
  • теглете пари при избор на потребителя
  • уведомете собственика, когато на автоматите изтече продуктът, срокът на годност на продукта е извън обхвата или касата за пари е почти пълна

Това е доста дълъг списък от функции и се надяваме те да имат единични тестове.

https://unsplash.com/photos/5MKCA4STlVM

Базова реализация

архитектура

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

Архитектура преди рефакторинг

Актьорът на вендинг машина съдържа няколко вар, за да поддържа държавата. Освен вар, има блок за получаване с много логика. Фрагментът по-долу показва най-важното. Заслужава да се отбележи, че логиката на смесване (редове 14-19) и изпълнението на страничен ефект (редове 21-23 актуализиране на вътрешното състояние, редове 27-331 изпращане на съобщения).

Тестване

Нека да разгледаме теста. Както можете да видите, използвам Akka Testkit. Кодът по-долу потвърждава сценарий за щастлив път, когато потребителите купуват продукт от чекмеджето „1“.

Този код има няколко недостатъка. Първо, трябва да използваме Akka за тестване на бизнес кода. Това изисква да се стартира системата за актьори и това забавя тестовете. Второ, трябва да настроим вътрешно състояние на актьора, като изпращаме съобщения (underTest! Credit (10)) и се уверяваме, че съобщенията са обработени (userOutput.expectMsg (CreditInfo (10))). Трето, тестваме странични ефекти. Твърдението в този тест е валидно, че VendingMachineActor е изпратил съобщението GiveProductAndChange (бира, 7) до UserOutputsActor. Освен това, ако искаме да потвърдим вътрешното състояние на актьора, трябва да се справим с тестовото съобщение GetState.

Част от тестовете също имат проблема, че твърдението им е „не се изпълняват странични ефекти“. В този случай използваме очаквамеNoMessage () от AkkaTestkit. По подразбиране този разговор чака 3 секунди, за да се увери, че не е изпратено съобщение. Това забавя нашето изграждане. Можем да намалим това време, но това може да направи тестовете люспести.

Бизнес логиката, внедрена във VendingMachineActor, зависи от Akka. Този актьор има голям блок за получаване и няколко вар, за да поддържа държавата. Добавянето или промяната на кода изисква разсъждения за целия клас актьор.

Рефакторинг на държавна монада

Какво е държавна монада

Държавна монада е функция, която приема състояния и връща двойки нови състояния и ефекти:

Състояние -> (състояние, ефект)

Държавата монада е монада, така че предоставя метода flatMap. Благодарение на това можем да съставим държавни монади:

Използвайки за разбиране, новата монада на държавата е изградена (но все още не се изпълнява). Прилагането на първоначално състояние на stateMonad1 ще доведе до ефект1 и ново състояние (нека го наречем състояние1). Ефектът се възлага на ефект1. На следващо място, stateMonad2 се извиква с state1 като аргумент и ефектът е присвоен на effect2. В секцията за добив можем да изградим краен резултат, използвайки резултатите от стъпките по-долу.

Сега можем да стартираме newMonad с аргумент InitiState като аргумент. В резултат на това получаваме ново състояние и ефекти.

Подробностите за прилагането на монадата в котките можете да намерите в книгата „Скала с котки“ (достъпна тук).

Повторно прилагане на нова логика наред със старата

Реших да създам нов актьор, вместо да променя съществуващия (за да мога да сравня тези две реализации). Новата имплементация използва действащо лице (SmActor), за да поддържа състоянието между повиквания, комуникация и монада за състояние на повикване. Цялата бизнес логика, която искам да прилагам като държавна монада. SmActor ще има същия API, така че мога да използвам отново съществуващите тестове. По този начин мога да проверя дали и двете реализации правят едно и също. По-късно ще напиша тестове за държавни монади.

Архитектура след рефакторинг

При този подход актьорът запазва само една променлива - състояние. Това състояние ще се състои от всички данни, необходими за обработване от държавата монада. По-долу е дефиницията на състоянието клас:

В SmActor току-що дефинирах един вар със състояние:

Вече мога да намаля броя на случаите в блок за получаване:

Има един случай за всички бизнес събития. Actor съдържа и случай GetState за съвместимост на тестовете.

Обект VendingMachineSm с метод compose липсва. Нека започнем с изпълнението. Както е показано по-горе, можем да приложим логиката като няколко държавни монади и да ги комбинираме в композитен метод. Първо искам да дефинирам типа връщане:

Прилагането на логиката може да бъде разделено на по-малки монади и по-късно комбинирано. Първият метод за внедряване ще бъде актуализиране на кредит, когато някой вмъкне или изтегли монета:

След поставяне на монета (Кредит (стойност)), кредитът се увеличава и като изход се генерира съобщение до потребителя за текущия кредит. Заслужава да се отбележи, че към този момент не се изпълнява никакъв страничен ефект (изпращане на съобщения до други участници).

Ако потребителите решат да изтеглят пари, държавата се актуализира с нулев кредит и се създава съобщение до потребителя. За всички останали действия можем да върнем състояние без модификации и резултати.

Сега можем да потвърдим изпълнението с тест:

Както можете да видите, логиката за актуализиране на кредита е разделена.

Останалата логика може да се реализира по същия начин (проверете логиката и тестовете). Можем да използваме всички малки държавни монади, за да създадем цял процес. За целта използваме за разбиране. Състоянието се променя и преминава към следващата монада на държавата. Резултатите се събират и комбинират в част от добива. Все пак нямаше изпълнение на държавна монада. Току-що изградихме по-голям.

Тестване по FP начин

С това изпълнение можем да тестваме логиката на „малките държавни монади“ и съставихме такава. Тези тестове не изпълняват странични ефекти. Всички върнати ефекти се връщат от държавата монада. По този начин методът на твърдение има всички данни, не е нужно да се притесняваме да проверяваме макети или TestActorRef за странични ефекти. В тестовете за актьори тествахме, че не се изпълняват странични ефекти за определен период от време, с FP трябва просто да проверим върнатия резултат. Освен това не е необходимо да стартираме системи за актьори за тестване. Съчетавайки това, тестът е много по-бърз.

Също така лесно можем да стартираме множество действия и да проверим междинни резултати:

При тест за актьор методът на получаване е под тест. получаването е просто псевдоним за Any => Unit. Това означава, че може да се извърши неизвестен страничен ефект. С помощта на чист FP целият резултат се връща по изпитан метод. В нашия случай се изпълнява ново състояние и ефекти (State => (State, Effect)). В такъв случай е лесно да се валидира, ако върнатата стойност отговаря на очакванията.

Разделянето на логиката и състоянието носят ползи за тестване. Не е нужно да настройваме вътрешно състояние (на тествания обект), като извикваме неговите методи (или изпращаме съобщения). В нашия случай, ако искаме да тестваме сценарий на изтегляне, трябва да вмъкнем монета във вендинг машина. При по-сложен сценарий може да са необходими повече стъпки за подготовка на вътрешното състояние. Когато обектът с логика е без гражданство, можем просто да предадем състояние като един от аргументите.

Ползи

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

Разделянето на логиката на по-малки части ни позволява да тестваме по-прости функции. Можем да тестваме и сложна логика, изградена от по-малки части. Бизнес логиката не зависи от рамки като Akka и теми. Може да се тества лесно.

По-малките части на логиката могат да бъдат използвани повторно в различни части на програмата. С помощта на тези по-малки части може да се изгради сложна логика. Ако нашите функции са чисти (няма странични ефекти и мутиращо състояние), ние сме сигурни, че комбинирането на тези функции няма да доведе до неочаквано поведение.

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

Освен това нашите тестове са много по-бързи, защото не е необходимо да стартираме Actor System или да се уверим, че не е изпълнен страничен ефект.

резюме

Преместихме бизнес логиката от актьор в отделен обект. Актьорът е отговорен само за поддържане на състояния между синхронизиране на разговори и нишки. Благодарение на държавната монада бизнес логиката беше разделена на по-малки части. Тези по-малки части се използват за изграждане на сложна логика. Тестовете светят бързо.

връзки:

  • Проект в Github https://github.com/otrebski/state-monad
  • Скала с книги за котки https://underscore.io/books/scala-with-cats/