Русский
Русский
English
Статистика
Реклама

Functional programming

Перевод Почему я считаю Haskell хорошим выбором с точки зрения безопасности ПО?

31.05.2021 18:12:13 | Автор: admin


Команда Typeable понимает ценность безопасности. Мы любим Haskell, но стоит ли его выбирать, если ваша цель создание защищенного программного обеспечения? Хотелось бы сказать да, но как и для большинства эмпирических вопросов о разработке ПО, здесь просто нет объективного доказательства, подтверждающего, что Haskell или ещё какой-нибудьй язык программирования обеспечивает большую безопасность, чем любой другой. Нельзя сказать, что выбор языка в Typeable не имеет значения для безопасности, но какое именно значение он имеет, еще нужно подумать.


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


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


   Чисто техническая            Уязвимость, относящаяся        уязвимость                исключительно к предметной области                                                                                           Инструментарий Инструментарий  Нужно     должен исправить может помочь  подумать

На оси выше показан источник различных уязвимостей программного обеспечения. На крайнем правом конце мы видим ошибки, связанные исключительно с доменной областью, то есть ошибки, совершенно не зависящие от используемого инструментария. Примером такой ошибки являются контрольные вопросы, которые в начале 2000-х использовались многими веб-сервисами для восстановления паролей. Зачастую это были вопросы типа Девичья фамилия вашей матери?. Позднее, примерно в 2009-2010 годах, возникло такое явление, как социальные сети, и неожиданно девичья фамилия матери перешла в категорию общедоступной информации. Неважно, какую технологию вы используете для реализации такой схемы с контрольными вопросами. Эта схема все равно не работает.


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


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


В таких сервисах обычно есть соблазн записать файл пользователя непосредственно в файловую систему сервера. Однако под каким именем файла? Использовать непосредственно имя файла пользователя верный путь к катастрофе, так как оно может выглядеть как ../../../etc/nginx/nginx.conf, ../../../etc/passwd/ или любые другие файлы, к которым сервер имеет доступ, но не должен их изменять.


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


Использование шкалы


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


В идеале современный инструментарий должен практически полностью устранять чисто технические уязвимости. Например, большинство современных языков, таких как Haskell, C# и Java, по большей части обеспечивают защиту содержимого памяти и в целом предотвращают переполнение буфера, попытки дважды освободить одну и ту же ячейку, а также другие технические проблемы. Однако от правильного инструментария можно получить еще больше пользы. Например, легко представить себе систему, в которой имеется техническая возможность разделить абсолютный и относительный пути к файлу, что упрощает контроль атак с обходом каталога (path traversal), таких как загрузка пользователем файла поверх какого-нибудь важного конфигурационного файла системы.


Haskell нижняя часть шкалы


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


// From imaginary CSRF token protection:if ($tokenHash == $hashFromInternet->{'tokenHash'}) {  echo "200 OK - Request accepted", PHP_EOL;}else { echo "403 DENIED - Bad CSRF token", PHP_EOL;};

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


Аналогичная проблема возникла с Java (и другим языками, см. https://frohoff.github.io/appseccali-marshalling-pickles/). Java предложил исключительно удобный способ сериализации любого объекта на диск и восстановления этого объекта в исходной форме. Единственной досадной проблемой стало отсутствие способа сказать, какой объект вы ждете! Это позволяет злоумышленникам пересылать вам объекты, которые после десериализации в вашей программе превращаются во вредоносный код, сеющий разрушения и крадущий данные.


Это не значит, что вы не можете создать безопасный код на PHP или не можете получить такие же ошибки в Haskell, однако по своей природе Haskell не склонен к таким уязвимостям. Если переложить приведенный выше пример кода на Haskell, он будет выглядеть примерно так:


data Request = Request {csrfToken :: Token, ... other fields}doSomething :: Session -> Request -> Handler ()doSomething session request  | csrfToken session == csrfToken request = ... do something  | otherwise = throwM BadCsrfTokenError

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


Haskell середина шкалы


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


Прежде всего, в Haskell имеется возможность моделировать данные более точно по сравнению с такими языками, как как C, Javascript или даже Java. В основном это обусловлено удобством его синтаксиса и наличием типов-сумм. Точное моделирование данных имеет значение для безопасности, поскольку код домена в основном представляет собой модель некоторого реального явления. Чем меньше ее точность, тем больше возможностей имеют злоумышленники.


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


data SSN = Unknown | Redacted | SSN Text

А теперь сравним моделирование той же идеи с использованием строковых величин "", "<REDACTED>" и "191091C211A". Что произойдет, если пользователь введет "<REDACTED>" в поле ввода SSN? Может ли это в дальнейшем привести к проблеме? В Haskell об этом можно не беспокоиться.


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


storeFileUpload :: Path Abs File -> ByteString -> IO ()storeFileUpload path = ...

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


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


Haskell и ошибки домена


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


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


Однако это все догадки. Сообщество Haskell до сих пор достаточно мало, чтобы не быть объектом атак, а специалисты по Haskell в общем случае еще не так сильно озабочены проблемами безопасности, как разработчики на Javascript или Python.


Заключение


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

Подробнее..

Из песочницы Для чего на самом деле нужны стрелочные функции в JavaScript

21.11.2020 20:20:09 | Автор: admin
Привет, Хабр! Представляю вашему вниманию перевод статьи The real reason why JavaScript has arrow functions автора Martin Novk.


* фраза-мем из игры Skyrim

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

Это пример того же кода, написанного также и в традиционном стиле:

const arrowFunction = (arg1, arg2) => arg1 + arg 2;const traditionalFunction = function(arg1, arg2) {  return arg1 + arg2;};

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

const arrowFunction = (arg1, arg2) => {  const result = arg1 + arg2;  return result;};

Стрелочные функции также часто называют лямбда-функциями и они используются не только в JavaScript. Язык Python может послужить хорошим примером где также встречаются лямбда-функции.

В Python их синтаксис выглядит следующим образом:

lambdaFunction = lambda a, b : a + b

Упрощай


Использование стрелочных функций мотивирует вас упрощать код в соответствии с KISS принципом (Keep-it-simple-stupid) и принципом единственной ответственности (каждая функция отвечает только за одно конкретное действие).

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

Функции первого класса


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

document.querySelector('#myButton').addEventListner('click', function() {  alert('click happened');});

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

Пример возвращения функции другой представлен ниже:

const myFirstClassFunction = function(a) {  return function(b) {    return a + b;  };};myFirstClassFunction(1)(2); // => 3

Однако, со стрелочной функцией, все выглядит гораздо чище:

const myFirstClassArrow = a => b => a + b;myFirstClassArrow(1)(2); // => 3

Здесь все просто: то, что написано до последней стрелки аргументы, а после нее вычисление. По факту, мы работаем с несколькими функциями, а также мы можем использовать несколько вызовов fn(call1)(call2);

Частичное применение


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

const add = a => b => a + b;const increaseCounter = counter => add(1)(counter);increaseCounter(5); // => 6const pointFreeIncreaseCounter = add(1);pointFreeIncreaseCounter(5); // => 6

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

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

Функциональное программирование


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

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

Чистые функции полагаются только на входные данные и всегда возвращают какое-то значение. Они никогда не изменяют (мутируют) другие переменные и не зависят внешних данных, за пределами входных значений. Это приводит к ссылочной прозрачности, которое упрощает программирование.

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

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

Стрелочные функции заключение


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

Реальный функциональный пример


Чтобы понять, что же скрывается в кроличьей норе, давайте взглянем на пример использования open-source библиотеки @7urtle/lambda

import {trim, upperCaseOf, lengthOf, lastLetterOf, includes, compose} from '@7urtle/lambda';const endsWithPunctuation = input =>  includes(lastLetterOf(input))('.?,!');const replacePunctuationWithExclamation = input =>  substr(lengthOf(input) - 1)(0)(input) + '!';const addExclamationMark = input =>  endsWithPunctuation(input)    ? replacePunctuationWithExclamation(input)    : input + '!';const shout = compose(addExclamationMark, upperCaseOf, trim);shout(" Don't forget to feed the turtle.");// => НЕ ЗАБУДЬ ПОКОРМИТЬ ЧЕРЕПАХУ!

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

JavaScript может быть сложным в его непоследовательности между использованием Си-подобного синтаксиса и особенностей функционального программирования. Чтобы помочь людям лучше разобраться и предоставить доступ к материалам для изучения, я разработал библиотеку @7urtle/lambda. Вы можете освоить функциональное программирование в своем ритме и сайт www.7urtle.com поможет вам изучить данную парадигму, предоставив вам все необходимые инструменты.
Подробнее..

Сочиняя ПО Введение

15.03.2021 14:07:31 | Автор: admin

Этастатья-частьсериистатей"Сочиняя ПО" про функциональное программирование и различныетехникисозданияпрограммнаJavaScriptES6+,начинаясазов. Оставайтесьнасвязи,многоновоговпереди!

Композиция: "Действие, заключающееся в составлении единого целого из частей или элементов."

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

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

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

  • Что такое функциональная композиция (композиция функций)?

  • Что такое объектная композиция (композиция объектов)?

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

Весь мир сегодня зависит от программ. Каждый новый автомобиль - это мини суперкомпьютер на колесах, и ошибки в программном обеспечении могут привести к реальным авариям и стоить жизни реальным людям. В 2013 юристы признали команду разработки компании Toyotaвиновной в "безрассудном пренебрежении"после того, как расследование аварии выявило т.н. "спагетти-код" с примерно 10000 глобальных переменных.

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

Мы обязаны делать свою работу лучше.

Вы используете композицию каждый день

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

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

Композиция функций

Функциональная композиция это процесс применения функции к результату вызова другой функции. В алгебре это выглядит так: пусть даны две функцииfиg, тогда их композиция это(f g)(x) = f(g(x)). Символ- это оператор композиции. Вы всегда можете сказать, что "композиция функцийfиgравнаfотgотx". Можно также сказать, чтоfэто внешняя функция, аgвнутренняя, потому чтоfприменяется к результату функцииg.

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

const g = n => n + 1;const f = n => n * 2;const doStuff = x => {  const afterG = g(x);  const afterF = f(afterG);  return afterF;};doStuff(20); // 42

Каждый раз, когда вы используете цепочкуpromise, вы используете композицию функций:

const g = n => n + 1;const f = n => n * 2;Promise.resolve(20)  .then(g)  .then(f)  .then(value => console.log(value)); // 42

Более того, каждый раз, когда вы комбинируете в цепочку вызовы методов обработки массивов (map,filter, etc), методовlodash, илиobservablesизRxJS, вы используете композицию функций. Создаете цепочки вызовов - значит используете композицию. Если результат вызова функции передается в другую функцию - это композиция. Если вы создаете цепочку вызовов из двух методов - вы комбинируете их используяthisв качестве входящих данных.

Когда вы используете композицию намеренно - вы делаете это лучше. Используя композицию функций намеренно мы можем улучшить нашdoStuff()и превратить его в простую функцию из одной строки:

const g = n => n + 1;const f = n => n * 2;const doStuffBetter = x => f(g(x));doStuffBetter(20); // 42

Для начала, давайте создадим абстракцию для логгирования "after g" и сделаем небольшую функциюtrace():

const trace = label => value => {  console.log(`${ label }: ${ value }`);  return value;};

Теперь мы можем переписать наш код:

const doStuff = x => {  const afterG = g(x);  trace('after g')(afterG);  const afterF = f(afterG);  trace('after f')(afterF);  return afterF;};doStuff(20); // =>/*"after g: 21""after f: 42"*/

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

import pipe from 'lodash/fp/flow';const doStuffBetter = pipe(  g,  trace('after g'),  f,  trace('after f'));doStuffBetter(20); // =>/*"after g: 21""after f: 42"*/

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

// pipe(...fns: [...Function]) => x => yconst pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

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

pipe()создает цепочку вызовов, пайплайн если хотите, передавая вывод одной функции на вход другой. Когда вы используетеpipe()(и ее близнецаcompose()) вам не нужны промежуточные переменные для хранения результата вызова. Создание функций без описания (идентификации) их аргументов называется молчаливым программированием. Чтобы провернуть такой фокус, вы должны вызвать функцию, которая возвращает новую функцию, вместо того, чтобы описывать функцию явно. Это означает, что вам не нужно использоватьfunctionили=>для создания функции (как в примере выше сделано для функции doStuffBetter).

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

Уменьшение сложности дает некоторые преимущества:

  • Использование памяти. Мозг человека может держать в памяти одновременно ограниченное количество элементов, и каждая переменная потенциально занимает один такой элемент. Чем больше переменных в коде, тем сложнее вспомнить, для чего именно используется та или иная переменная. Обычно мы можем держать в уме от 4 до 7 различных элементов одновременно. Если количество элементов превышает число 7, то количество ошибок значительно возрастает. Используя пайплайн, мы избавились от 3-х переменных, а значит освободили как минимум половину места в нашей голове для других вещей. Это значительно уменьшает умственную нагрузку. Конечно, программисты могут держать в уме значительно больше, чем обычные люди, но, все же, не настолько больше, чтобы игнорировать важность этого момента.

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

  • Пространство для ошибок. Сравните код до и после изменений. Такое впечатление, что наша функция села на диету и похудела на тонну. Это важно, потому, что чем больше кода, тем больше места для ошибок.

Композиция объектов

"Используйте композицию объектов вместо наследования классов" - Банда Четырех, "Приёмы объектно-ориентированного проектирования. Паттерны проектирования"

"В информатике составной тип данных - это любой тип данных, который может быть сконструирован с использованием примитивных типов данных языка программирования и других составных типов. [] Процесс создания составного типа данных называется композицией." -Wikipedia

Это представители примитивных типов:

const firstName = 'Claude';const lastName = 'Debussy';

А вот это составной тип:

const fullName = {  firstName,  lastName};

Таким образом такие структуры какArray,Set,Map,WeakMap,TypedArrayи многие другие являются составными. Каждый раз когда вы создаете новую структуру данных, вы используете что-то вроде композиции объектов.

Следует отметить, что "Банда Четырех" описала шаблон проектирования, называемый "Компоновщик" (Composite), который является специфическим типом рекурсивной композиции объектов, позволяющей использовать отдельные компоненты и их объединения одинаковым образом. Некоторые разработчики попадают в ловушку и считают, что шаблон "Компоновщик" это единственный путь использования композиции объектов. Это не так, существует множество различных типов и способов композиции объектов.

"Банда Четырех" продолжает: "Вы увидите что композиция объектов применяется снова и снова в шаблонах проектирования", а затем сводит все типы композиций к трем видам отношений, включающихделегирование(используется, например, в шаблонах "Состояние", "Стратегия", "Посетитель"),ассоциацию(когда объект знает про другой объект по ссылке, обычно передаваемой как параметр, это вид отношения "использует", как, например, объект-обработчикиспользуетлоггер для записи в лог запроса) иагрегацию(когда дочерние объекты формируют родительский объект, каждый свою часть, это вид отношения "имеет", как, например, элемент DOM-дереваимеетпотомков).

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

Мы будем использовать более общее определение композиции объектов, данное, например, в работе"Методы теории категорий в программировании: аспекты топологии" (1989):

Составные объекты формируются путем объединения объектов таким образом, что каждый из последующих является "частью" предыдущего.

Другой хороший источник это работа Гленфорда Майерса"К надежному ПО через композицию" (1975). Обе книги давно уже разошлись, но их все еще можно найти на Amazon или Ebay, если вы желаете погрузиться глубже в тему составных объектов.

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

  • Сильная связанность: так как потомки зависят от реализации родителя, то иерархия классов приводит к самой сильной связанности из вообще возможных в ООП

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

  • Негибкая иерархия: иерархия классов, допускающая только одного предка, с течением времени и вносимыми изменениями перестает точно удовлетворять всем вариантам использования

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

  • Проблема банана и гориллы: "Проблема объектно-ориентированных языков программирования в том, что у них есть неявные зависимости, которые они тянут за собой. Вам необходим банан, но вместо этого вы получаете гориллу, держащую банан и все джунгли" - Джо Армстронг,"Программисты за работой".

Наиболее часто встречающаяся форма составных объектов в JavaScript известна как объединение объектов (смешанная композиция). Это работает так, как будто вы собираете мороженое. Вы начинаете с объекта (мороженое), а затем добавляете к нему различные добавки. Добавьте орехи, карамель, шоколадный завиток и вы получите карамельно-ореховое мороженное с шоколадом.

Вот так выглядит создание составных объектов с использованием классов:

class Foo {  constructor () {    this.a = 'a'  }}class Bar extends Foo {  constructor (options) {    super(options);    this.b = 'b'  }}const myBar = new Bar(); // {a: 'a', b: 'b'}

А вот так выглядит создание составных объектов с использованием смешения объектов:

const a = {  a: 'a'};const b = {  b: 'b'};const c = {...a, ...b}; // {a: 'a', b: 'b'}

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

  1. Существует более одного способа создавать составные объекты

  2. Некоторые пути лучше других

  3. Вы выбираете самый простой, наиболее гибкий способ решения поставленной задачи.

Заключение

Эта статья не о том, что лучше, функциональное программирование (ФП) или объектно-ориентированное программирование (ООП) и не о том, какой язык программирования лучше. Компоненты могут представлять собой функции, структуры данных, класс, и т.д. Различные языки программирования располагают к использованию различных базовых элементов для компонентов. Java предполагает использование классов, Haskell - функций, и т.д. Неважно, какой язык и какую парадигму вы предпочитаете, вам не уйти от функциональной композиции и структур данных. В конце концов именно к этому все и сводится.

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

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

Мы будем использовать объектную композицию для новых типов данных для функционального программирования, и мы будем использовать ФП для создания объектов для ООП.

Не важно, как вы пишете код, вы должны делать это хорошо.

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

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

Настало время узнать как разрабатывать ПО.

Подробнее..

Перевод Сочиняя ПО Почему стоит изучать ФП на JavaScript?

16.04.2021 02:15:22 | Автор: admin

Эта статья - часть серии статей "Составляя ПО" про функциональное программирование и различные техники создания программ на JavaScript ES6+, начиная с азов. Предыдущая часть: Сочиняя ПО: Введение

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

Если вы опытный разработчик, уже знакомый с JavaScript или с каким-то чисто функциональным языком, то вы можете подумать, что JavaScript это весьма забавный способ открыть для себя мир *[ФП]: функциональное программирование. Отставьте эти мысли в сторону и попробуйте посмотреть на текст незашоренным взглядом. Вы можете обнаружить скрытый уровень в программировании на JavaScript, уровень, о котором вы даже не подозревали.

Раз уж эта статья имеет в названии "Сочиняя ПО", и ФП это, очевидно, путь сочинить программу (используя функциональную композицию, функции высшего порядка, и т.д.), то вы можете спросить, почему бы нам не взять какой-нибудь Haskell, ClojureScript, или Elm вместо JavaScript.

JavaScript имеет в своем составе важные особенности, необходимые для ФП:

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

  2. Анонимные функции и лямбда-синтаксис. Например, запись видаx => x * 2является валидным выражением в JavaScript. Такой синтаксис значительно упрощает работу с функциями высшего порядка.

  3. Замыкания. Замыкание - это комбинация функции и ее лексического окружения. Замыкания создаются в момент создания функции. Когда функция создается внутри другой функции, то она имеет доступ к переменным, объявленным во внешней функции, даже после того, как будет осуществлен возврат из этой внешней функции. Замыкания это то, что позволяет работать фиксированным аргументам частичных применений. Фиксированный аргумент - это аргумент, заданный в контексте замыкания возвращаемой функции. Например, в выраженииadd(1)(2)аргумент1является фиксированным аргументом для функции, возвращаемой при вызовеadd(1). Пример:

/* * Более длинный вариант: * const add = function (x) { *     return function (y) { *         return x + y; *     }     * } */const add = x => y => x + y;const summ = add(1)(2);

Что отсутствует в JavaScript

JavaScript - это универсальный язык, дающий возможность использовать несколько парадигм, т.е. позволяющий программировать в различных стилях. Эти другие стили включают в себя: процедурный (императивный) стиль программирования (как, например в Си), где функции представляют собой набор инструкций, который может быть вызван из другого места в программе; объектно-ориентированный стиль, где объекты - а не функции - являются основным строительным блоком. Недостатком такого мульти-парадигменного подхода является то, что императивный и объектно-ориентированный стиль предполагают, что практически всё в коде должно быть изменяемым или подверженным мутации, мутабельным.

Мутация - это изменение значения структуры данных без создания новой переменной и переприсваивания значения. Например:

const foo = {  bar: 'baz'};foo.bar = 'qux'; // мутация

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

Ниже представлен список вещей, которые присущи некоторым функциональным языкам и которых нет в JavaScript:

  1. Чистота. В некоторых ФП языках "чистота" поддерживается самим языком. Выражения с побочными эффектами недопустимы.

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

  3. Рекурсия. Рекурсия - это способность функции вызвать саму себя. Во многих ФП языках рекурсия это единственная возможность выполнить цикл. В таких языках нет конструкций типаfor,whileилиdo ... while.

Чистота: В JavaScript "чистота" может быть достигнута только по соглашению (т.е. все участники должны договориться использовать только чистые функции - прим. перев.). Если вы не используете для большей части своего приложения композицию чистых функций, то вы не следуете функциональному стилю. К сожалению, в JavaScript слишком просто сбиться с пути случайно начав создавать и использовать не "чистые" функции.

Иммутабельность: В ФП языках иммутабельность зачастую дана по умолчанию. В JavaScript отсутствуют эффективные структуры данных, используемые в большинстве ФП языков, но существуют библиотеки, которые могут помочь в этом вопросе, напримерImmutable.jsиMori. Я надеюсь, что в будущих версиях спецификации ECMAScript все же появятся неизменяемые структуры данных.

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

Если переменнаяconstуказывает на объект, то значение переменной не может быть изменено полностью, однако могут изменены свойства объекта. В JavaScript имеется возможность заморозить объект с помощьюfreeze(), но такие объекты замораживаются только на верхнем уровне, а это означает, что если замороженный объект имеет в каком-либо свойстве другой объект, то свойства этого другого объекта подвержены изменениям. Другими словами, JavaScript ещё предстоит долгая дорога к настоящей неизменности, закрепленной в спецификации языка.

Рекурсия: Технически, JavaScript поддерживает рекурсию, однако большинство функциональных языков имеют такую особенность как "оптимизация хвостовой рекурсии". Такая особенность позволяет рекурсивным функциям переиспользовать фреймы стека для последующих рекурсивных вызовов (фактически рекурсия преобразуется в плоскую итерацию - прим. перев.).

Без оптимизации хвостовой рекурсии стек вызовов растет без ограничений и может вызвать переполнение стека. JavaScript, с технической точки зрения, имеет ограниченную хвостовую оптимизацию в стандарте ES6. К сожалению, только один из наиболее распространенных браузеров реализовал эту функциональность, а подобная оптимизация в Babel (наиболее популярный JavaScript компилятор, используемый для компиляции ES6 в ES5), хоть и частичная, была позднее удалена.

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

Что есть в JavaScript такого, чего нет в функциональных языках

Педанты скажут нам, что изменчивость объектов в JavaScript не самая большая проблема, и будут правы. Однако, иногда изменчивость и сайд-эффекты могут быть полезны (сайд-эффект - это изменение внешнего состояния, например глобальной переменной, внутри функции - прим. перев.). На самом деле, практически невозможно создать современное приложение без сайд-эффектов. Специализированные функциональные языки вроде Haskell используют сайд-эффекты, но маскируют их в чистых функциях используя специальные конструкции, называемые монады, позволяя программе оставаться чистой даже не смотря на сам факт присутствия сайд-эффектов в монадах.

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

"Монада - это моноид из категории эндофункторов, какие тут проблемы?" ~ Джеймс Айри, как бы цитирующий Филипа Вадлера, перефразируя реальную цитату Сондерса Мак Лейна."Краткая, неполная и в большинстве своем неправильная история языков программирования"

Как правило, пародия преувеличивает смешные вещи, делая из еще забавнее. Цитата выше - это упрощенное определение термина монада, которое звучит примерно так:

"Монада в множестве Х это моноид из категории эндофункторов Х, в котором морфизм, называемый "произведение", заменен композицией эндофункторов, а морфизм, называемый "единица", заменен эндофунктором "тождественность". ~ Сандерс Мак Лейн."Категории для практикующих математиков".

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

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

Согласно Брендану Эйху, такая цель существовала с самого начала:

"... авторы компонентов, кто пишут на С++ или (мы надеемся) на Java, и "скриптовики", начинающие или "про", кто будут писать код, внедренный в HTML."

Исходное намерение Netscape состояло в поддержке двух различных языков, и скриптовый язык, предположительно, должен был напоминать Scheme (диалект языка Lisp). Брендан Эйх:

"Я был нанят компанией Netscape с обещанием "реализовать Scheme" в браузере".

JavaScript должен был стать новым языком:

"Диктат высшего руководства заключался в том, что язык должен быть похож на Java. Это сразу оставило за бортом Perl, Python и Tcl вместе со Scheme."

Таким образом, замысел Брендана Эйха с самого начала был:

  1. Scheme в браузере

  2. Выглядит как Java

Ну и кончилось это все даже большей мешаниной:

"Я совсем не горжусь, но счастлив, что выбрал в качестве основных ингредиентов scheme-подобные функции первого класса и self-подобные (хотя и единичные) прототипы (видимо, имеется ввиду, что в языке Self прототипы сложнее чем в JavaScript - прим. перев.). Влияние Java, особенно проблема y2k, а также разделение на примитивные типы и объекты, было неудачным."

Я бы добавил к списку "неудачных" Java-подобных особенностей языка то, что в конце-концов вошло в JavaScript:

  • Функция-конструктор и ключевое словоnew, с семантикой и способом вызова отличными от функций-фабрик

  • Ключевое словоclassвместе сextendsдля наследования от единственного родителя как основной механизм наследования

  • Тенденция разработчиков думать о классе как о статическом типе, коим он не является.

Мой вам совет: избегайте использования всего этого как только можете.

Мы счастливчики в том, что JavaScript стал настолько богатым языком, потому что в конечном итоге его скриптовый подход выиграл у "компонентного" подхода (на сегодня Java, Flash и ActiveX расширения не поддерживаются в большинстве используемых браузеров).

Мы пришли к тому, что единственным языком, напрямую поддерживаемым браузером стал JavaScript.

Это означает, что браузеры меньше перегружены и содержат меньше ошибок, потому что им нужно поддерживать только один язык - JavaScript. Вы можете подумать, что WebAssembly - это исключение, но одна из целей разработки WebAssembly - это использование существующей поддержки JavaScript абстрактным синтаксическим деревом (AST). На практике, первой демонстрацией возможностей WebAssembly стало подмножество JavaScript, известное как ASM.js.

Положение единственного стандартного языка программирования общего назначения для веба позволило JavaScript оседлать самую большую волну популярности в истории программного обеспечения:

Приложения съели мир, Интернет съел приложения, а JavaScript сожрал Интернет.

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

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

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

Истинная сила JavaScript - в разнообразии мнений и разработчиков в экосистеме. Возможно, это не совсем идеальный язык для сторонников функционального программирования, но он может быть идеальным языком для совместной работы, который работает практически на любой платформе, которую вы можете себе представить, - знакомым людям из других популярных языков, таких как Java, Lisp или C. JavaScript не будет идеален для разработчика из любого другого языка, но он может чувствовать себя достаточно комфортно для того, чтобы выучить язык и быстро стать продуктивным.

Я согласен с мнением, что JavaScript не лучший язык для ФП. Однако, ни один из других ФП языков не может похвастаться тем, что подходит каждому и каждый может принять его и начать использовать, а также, как продемонстрировал ES6 - JavaScript может становиться лучше и обслуживать нужды разработчиков, заинтересованных в ФП. Почему бы вместо забвения JavaScript и его удивительной экосистемы, используемой практически в каждой компании в мире, не принять его и постепенно не сделать его лучшим языком для создания ПО?

В текущем состоянии JavaScript достаточно неплох для ФП, разработчики могут создавать всевозможные виды полезных и интересных штук, используя техники функционального программирования. Netflix (и любое приложение на Anglular 2+) использует функциональные утилиты, основанные на библиотеке RxJS. Facebook использует концепт чистых функций, функций высшего порядка, и компонентов высшего порядка для разработки Facebook и Instagram. PayPal, KhanAcademy и Flipkart используют Redux для управления состоянием.

Они не одиноки: Angular, React, Redux и Lodash лидируют в рейтинге используемых фреймворков и библиотек в экосистеме JavaScript, и все они весьма серьёзно вдохновлены ФП, а в случае Lodash и Redux, спроектированы таким образом, чтобы продемонстрировать причины применения паттернов ФП в реальных JavaScript приложениях.

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

В любой момент времени в США открыто около ста тысяч вакансий и еще сотни тысяч по всему миру. Изучение Haskell научит вас многому из ФП, но изучение JavaScript научит вас многому из того, как создавать работающие приложения для реальной работы.

Приложения съели мир, Интернет съел приложения, JavaScript съел Интернет.

Подробнее..

Functional FizzBuzz на Scala

13.06.2020 22:23:27 | Автор: admin

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


Предлагаю вашему вниманию еще один вариант, не совсем пятничный, а скорее субботний: FizzBuzz на Scala, functional style.


Задача


Для чисел от 1 до 100 нужно выводить на экран


  • Fizz, если число делится на 3;
  • Buzz, если число делится на 5;
  • FizzBuzz, если число делится и на 3 и на 5;
  • в противном случае само число.

Решение


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

Начнем с делимости


def divisibleBy(n: Int, d: Int): Boolean = n % d == 0divisibleBy(10, 5) // => true

Нет, это нас не устроит ведь делимость это свойство не только чисел типа Int, опишем делимость в общем виде, а за одно сделаем ее инфиксным оператором (Тут и далее используются некоторые возможности библиотеки cats):


import cats.implicits._import cats.Eqimplicit class DivisionSyntax[T](val value: T) extends AnyVal {  def divisibleBy(n: T)(implicit I: Integral[T], ev: Eq[T]): Boolean = {    import I._    (value % n) === zero  }  def divisibleByInt(n: Int)(implicit I: Integral[T], ev: Eq[T]): Boolean =    divisibleBy(I.fromInt(n))}10 divisibleBy 5 // => trueBigInt(10) divisibleBy BigInt(3) // => falseBigInt(10) divisibleByInt 3 // => false

Тут используются:


  • type class "Integral" требующий от типа "T" возможности вычислять остаток от деления и иметь значение "zero"
  • type class "Eq" требующий от типа "T" возможности сравнивать его элементы (оператор "===" это его синтаксис)
  • расширение типа "T" с помощью extension methods & value classes, которое не имеет рантайм-оверхеда (ждем dotty, который принесет нам нормальный синтаксис экстеншен методов)

Строго говоря метод divisibleByInt не совсем тут нужен, но он пригодится нам позже, если мы захотим использовать литералы целочисленного типа 3 и 5.


FizzBuzz


Отлично! Перейдем к вычислению того, что нужно вывести на экран, напомню, что это может быть "Fizz", "Buzz", "FizzBuzz" либо само число. Тут есть общий паттерн некоторое значение участвует в результате, только если выполняется определенное условие. Для этого подойдет Option, который будет определять используется значение или нет:


def useIf[T](value: T, condition: Boolean) = if (condition) Some(value) else None

Как и в случае с "divisibleBy(10, 5)" и "10 divisibleBy 5" задача решается, но как-то некрасиво. Мы ведь хотим не только решить задачу, но и создать инструмент для ее решения, DSL! По-сути, большая часть работы программиста и есть создание DSL разного рода, когда мы отделяем "как сделать" от "что сделать", "10 % 5 == 0" от "10 divisibleBy 5".


implicit class WhenSyntax[T](val value: T) extends AnyVal {  def when(condition: Boolean): Option[T] = if (condition) Some(value) else None}"Fizz" when (6 divisibleBy 3) // => Some("Fizz")"Buzz" when (6 divisibleBy 5) // => None

Осталось собрать все вместе! Мы могли бы использовать orElse и получили бы 3 правильных ответа из 4, но когда мы должны вывести "FizzBuzz" это не сработает, нам нужно получить Some("Fizz") ? Some("Buzz") => Some("FizzBuzz"). Просто строки можно складывать, но как сложить Option[String]? Тут на помощь нам приходят монады моноиды, cats предоставляет нам все нужные инстансы и даже удобный синтаксис:


  def fizzBuzz[T: Integral: Eq: Show](number: T): String =    ("Fizz" when (number divisibleByInt 3)) |+|    ("Buzz" when (number divisibleByInt 5)) getOrElse    number.show

Тут type class Show дает типу T возможность превращения в строку, |+| синтаксис моноида для сложения и getOrElse задает значение по-умолчанию. Все в общем виде и для любых типов, мы могли бы и от строк "Fizz" & "Buzz" абстрагироваться, но это лишнее на мой взгляд.


Конец


Все, что нам осталось сделать это (1 to 100) map fizzBuzz[Int] и куда-нибудь вывести результат. Но это уже совсем другая история...

Подробнее..

SOLID ООП?

03.07.2020 16:05:03 | Автор: admin

Наверное я не ошибусь, если скажу, что чаще всего на собеседованиях спрашивают о SOLID принципах. Технологии, языки и фреймворки разные, но принципы написания кода в целом похожи: SOLID, KISS, DRY, YAGNI, GRASP и подобные стоит знать всем.


В современной индустрии уже много десятков лет доминирует парадигма ООП и у многих разработчиков складывается впечатление, что она лучшая или и того хуже единственная. На эту тему есть прекрасное видео Why Isn't Functional Programming the Norm? про развитие языков/парадигм и корни их популярности.


SOLID изначально были описаны Робертом Мартином для ООП и многими воспринимаются как относящиеся только к ООП, даже википедия говорит нам об этом, давайте же рассмотрим так ли эти принципы привязаны к ООП?


Single Responsibility


Давайте пользоваться пониманием SOLID от Uncle Bob:


This principle was described in the work of Tom DeMarco and Meilir Page-Jones. They called it cohesion. They defined cohesion as the functional relatedness of the elements of a module. In this chapter well shift that meaning a bit, and relate cohesion to the forces that cause a module, or a class, to change.

Каждый модуль должен иметь одну причину для изменений (а вовсе не делать одну вещь, как многие отвечают) и как объяснял сам автор в одном из видео это означает, что изменения должны исходить от одной группы/роли людей, например модуль должен меняться только по запросам бизнес-аналитика, дизайнера, DBA специалиста, бухгалтера или юриста.


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


Open Closed


SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION
Bertrand Meyer

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


При этом функция это одна из лучших абстракций (исходя из принципа сегрегации интерфейсов, о котором позже). Использование функций для обеспечения этого принципа настолько удобно, что подход уже прочно перекочевал из функциональных языков во все основные ООП языки. Для примера можно взять функции map, filter, reduce, которые позволяют менять свой функционал прямой передачей кода в виде функции. Более того, весь этот функционал можно получить используя только одну функцию foldLeft без изменения ее кода!


def map(xs: Seq[Int], f: Int => Int) =   xs.foldLeft(Seq.empty) { (acc, x) => acc :+ f(x) }def filter(xs: Seq[Int], f: Int => Boolean) =   xs.foldLeft(Seq.empty) { (acc, x) => if (f(x)) acc :+ x else acc }def reduce(xs: Seq[Int], init: Int, f: (Int, Int) => Int) =  xs.foldLeft(init) { (acc, x) => f(acc, x) }

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


Liskov Substitution


Обратимся к самой Барбаре:


If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

Немного заумно, но в целом этот принцип требует, что бы объекты "подтипа" можно было подставить в любую программу вместо объектов родительского типа и поведение программы не должно поменяться. Обычно имеется в виду не полная идентичность поведения, а то, что ничего не сломается.


Как видите тут речь хоть и идет об "объектах", но ни слова о классах нет, "объект" тут это просто значение типа. Многие говорят о том, что этот принцип регламентирует наследование в ООП и они правы! Но принцип шире и может быть даже использован с другими видами полиморфизма, вот пример (немного утрированный конечно), который без всякого наследования нарушает этот принцип:


static <T> T increment(T number) {  if (number instanceof Integer) return (T) (Object) (((Integer) number) + 1);  if (number instanceof Double) return (T) (Object) (((Double) number) + 1);  throw new IllegalArgumentException("Unexpected value "+ number);}

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


Вообще люди, привыкли считать, что "полиморфизм" это один из принципов ООП, а значит про наследование, но это не так. Полиморфизм это способность кода работать с разными типами данных, потенциально неизвестными на момент написания кода, в данном случае это параметрический полиморфизм (собственно ошибочное его использование), в ООП используется полиморфизм включения, а существует еще и специальный (ad hoc) полиморфизм. И во всех случаях этот принцип может быть полезен.


Interface Segregation


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


С одной стороны этот принцип говорит об интерфейсах, как о наборе функций, "протоколе" который обязуются выполнять реализации и казалось бы уж этот принцип точно про ООП! Но существуют другие схожие механизмы обеспечения полиморфизма, например классы типов (type classes), которые описывают протокол взаимодействия с типом отдельно от него.


Например вместо интерфейса Comparable в Java есть type class Ord в haskell (пусть слово class не вводит вас в заблуждение haskell чисто функциональный язык):


// упрощенноclass Ord a where    compare :: a -> a -> Ordering

Это "протокол", сообщающий, что существуют типы, для которые есть функция сравнения compare (практически как интерфейс Comparable). Для таких классов типов принцип сегрегации прекрасно применим.


Dependency Inversion


Depend on abstractions, not on concretions.

Этот принцип часто путают с Dependency Injection, но этот принцип о другом он требует использования абстракций где это возможно, причем абстракций любого рода:


int first(ArrayList<Integer> xs) // ArrayList это деталь реализации -> int first(Collection<Integer> xs) // Collection это абстракция -> <T> T first(Collection<T> xs) // но и тип элемента коллекции это только деталь реализации

В этом функциональные языки пошли гораздо дальше чем ООП: они смогли абстрагироваться даже от эффектов (например асинхронности):


def sum[F[_]: Monad](xs: Seq[F[Int]]): F[Int] =  if (xs.isEmpty) 0.pure  else for (head <- xs.head; tail <- all(xs.tail)) yield head + tailsum[Id](Seq(1, 2, 3)) -> 6sum[Future](Seq(queryService1(), queryService2())) -> Future(6)

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




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

Подробнее..

Изучаю Scala Часть 3 Юнит Тесты

21.08.2020 22:11:51 | Автор: admin

Привет, Хабр! Мало написать хороший код. Нужно еще покрыть его хорошими Юнит Тестами. В прошлой статье я сделал простой веб сервер. Теперь попробую написать насколько тестов. Обычных, Property-based и с моками. За подробностями добро пожаловать под кат.

Содержание



Ссылки


Исходники
Образы docker image

И так для юнит тестов нужны 3 либы.

  1. Библиотека для создания тестов
  2. Библиотека которая будет генерировать тестовые данные
  3. Библиотека которая будет создавать моки объектов


Для создания тестов я использовал библиотеку ScalaTest
"org.scalatest" %% "scalatest" % "3.2.0" % Test

Для генерирования тестовых данных для Property-based тестирования я использовал ScalaCheck
"org.scalacheck" %% "scalacheck" % "1.14.3" % Test

и расширение которое совмещает ScalaTest + ScalaCheck ScalaTestPlusScalaCheck
"org.scalatestplus" %% "scalacheck-1-14" % "3.2.0.0" % Test

Для создания моков объектов я использовал ScalaMock
"org.scalamock" %% "scalamock" % "4.4.0" % Test

Простенький класс который представляет собой тип заполненной (не пустой) строки. Его мы сейчас и будем тестировать.
package domain.commonsealed abstract case class FilledStr private(value: String)object FilledStr {  def apply(value: String): Option[FilledStr] = {    val trimmed = value.trim    if (trimmed.nonEmpty) {      Some(new FilledStr(trimmed) {})    } else {      None    }  }}

Создаем класс для наших тестов
class FilledStrTests extends AnyFlatSpec with should.Matchers with ScalaCheckPropertyChecks {}

Создаем метод который будет проверять что при создании нашего класса из одинаковых строк мы будем получать одинаковые данные.
 "equals" should "return true fro equal value" in {    val str = "1234AB"    val a = FilledStr(str).get    val b = FilledStr(str).get    b.equals(a) should be(true)  }

В прошлом тесте мы захрадкодили в ручную созданную строку. Теперь сделаем тест используя сгенерированные данные. Мы будем использовать property-based подход при котором тестируются свойства функции что при вот таких входных данных мы получим вот такие выходные данные.
  "constructor" should "save expected value" in {    forAll { s: String =>//Тут фильтруем тестовые данные. Говорим что мы хотим использовать для теста только заполненные строки.      whenever(s.trim.nonEmpty) {        val a = FilledStr(s).get        a.value should be(s)      }    }  }

Можно явно настроить генератор тестовых данных чтобы использовать только нужные нам данные. Например так:
//Определяем наш набор данныхval evenInts = for (n <- Gen.choose(-1000, 1000)) yield 2 * n//Прогоняем тесты с этим наборомforAll (evenInts) { (n) => n % 2 should equal (0) }

Так же можно не передавать явно наш генератор а определить его implict через Arbitrary чтобы он автоматически передавался в качестве генератора в тесты. Например так:
implicit lazy val myCharArbitrary = Arbitrary(Gen.oneOf('A', 'E', 'I', 'O', 'U'))val validChars: Seq[Char] = List('X')//Это тест будет искать ближайший Arbitrary[Char] и получать данные для теста из него.forAll { c: Char => validChars.contains(c) }

Так же с помощью Arbitrary можно генерировать и сложные объекты.
case class Foo(intValue: Int, charValue: Char)val fooGen = for {  intValue <- Gen.posNum[Int]  charValue <- Gen.alphaChar} yield Foo(intValue, charValue)implicit lazy val myFooArbitrary = Arbitrary(fooGen)forAll { foo: Foo => (foo.intValue < 0) ==  && !foo.charValue.isDigit }

Теперь по пробуем написать тест по серьезней. Будем мокать зависимости для TodosService. Он Использует 2 репозитория и репозиторий в свою очередь использует абстракцию над транзакцией UnitOfWork. Будем тестить его самый простой метод
  def getAll(): F[List[Todo]] =    repo.getAll().commit()

Который просто вызывает репозиторий, начинает в нем транзакцию на чтения списка Todo, завершает ее и возвращает результат. Так же в тесте вместо F[_] поставлена монада Id которая просто возвращает хранящееся в ней значение.
class TodoServiceTests extends AnyFlatSpec with MockFactory with should.Matchers {  "geAll" should "возвращает ожидаемые значения" in {//Создаем моки зависимостей.    implicit val tr = mock[TodosRepositoryContract[Id, Id]]    implicit val ir = mock[InstantsRepositoryContract[Id]]    implicit val uow = mock[UnitOfWorkContract[Id, List[Todo], Id]]//Создаем сервис. Он принимает зависимости в свой конструктор не явно через implicit    val service= new TodosService[Id, Id]()//Создаем Id монаду со списком Todo внутри    val list: Id[List[Todo]] = List(Todo(1, "2", 3, Instant.now()))//Устанавливаем что метод getAll репозитория будет возвращать uow и будет вызван 1 раз    (tr.getAll _).expects().returning(uow).once()//Устанавливаем что метод commit будет возвращать созданную коллекцию и будет вызван 1 раз    (uow.commit _).expects().returning(list).once()//Устанавливаем что результат метода сервиса getAll должен быть равен значению коллекции //которую возвращает наш репозиторий    service.getAll() should be(list)  }}

Тесты писать на Scala оказалось очень даже приятно и ScalaCheck, ScalaTest, ScalaMock оказались очень хорошими библиотеками. Как и библиотека для создания АПИ tapir и библиотека для сервера http4s и библиотека для стримов fs2. Пока что окружение и библотеки для Scala вызывают у меня только положительные эмоции. Надеюсь дальше эта тенденция продолжится.
Подробнее..

Сильные стороны функционального программирования

15.02.2021 16:13:26 | Автор: admin


Привет! Меня зовут Катерина, и я испытываю самые тёплые чувства к функциональному программированию, использую функциональный язык на постоянной основе и даже немного преподаю.

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

Как ФП улучшает программирование


Функциональное программирование на данный момент довольно популярно, и во многих императивных языках стали появляться элементы ФП, такие как лямбда-функции, частичное применение (каррирование), функции высшего порядка (map, filter, свёртки). Где-то эти заимствования выглядят удачно, а где-то приводят к довольно странному, инородному синтаксису. Но парадигма программирования это подход, живущий в голове программиста, и не являющийся в общем случае частью языка. В той или иной степени любой язык поддерживает разные парадигмы, и его конструкции позволяют разрабатывать программы в различных стилях. Стоит ли вести разработку в функциональном стиле вопрос открытый, и каждый разработчик отвечает на него исходя из своих предпочтений, возможностей языка и других соображений.

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

  1. Код станет более лаконичным и выразительным. Выразительность можно определить как количество идей на единицу кода, и в целом функциональные языки, будучи более высокоуровневыми, оказываются и более выразительными. Например, преобразование каждого элемента в массиве или списке реализуется функциональным однострочником (используя map/foreach/whatever и анонимную функцию), в то время как в императивном стиле пришлось бы организовывать цикл, объявлять переменную для счётчика или итератора и использовать явное присваивание. Для более сложных примеров различие в выразительности только усиливается.
  2. Декомпозиция кода будет происходить более естественно. Принцип Разделяй и властвуй уже прочно закрепился в разработке и является базовым принципом борьбы со сложностью ПО. Речь идёт не о способе построения алгоритмов, а о более общем понятии. Например, даже простую программу, которая сначала читает целиком текст, разбивает его на слова и что-то делает с каждым словом, можно разбить на логические части: само чтение, разбиение прочитанного текста на слова (например, по пробелам) и создание структуры для хранения слов, обход этой структуры с преобразованием слов и печать результата. Каждое из этих действий можно реализовать отдельно и достаточно абстрактно, чтобы затем переиспользовать для решения других подобных задач. Декомпозиция кода на более мелкие и более общие части делает его гораздо более понятным (в том числе и для самого автора кода в будущем), позволяет избежать ошибок копипаста и упрощает дальнейший рефакторинг. Думаю, многим разработчикам приходилось копаться в своей или чужой простыне неструктурированного кода, написанного наспех, чтобы скорее заработало. Человеческому мозгу тяжело удерживать внимание на большом количестве сущностей одновременно и решать одну глобальную задачу сразу (working memory), поэтому для нас вполне естественно разбивать задачи на более мелкие, решать их по отдельности и комбинировать результат. В функциональном программировании эти мелкие задачи выражаются как небольшие вспомогательные функции, каждая из которых делает своё дело и её работу можно описать одним коротким предложением. А построение итогового результата это композиция таких функций. Конечно, разбить код на отдельные переиспользуемые части можно и в ООП, и в чисто императивном низкоуровневом языке типа C, и для этого уже есть известные принципы типа SOLID и GoF-паттерны, но, когда сам язык заставляет программиста думать в терминах функций, декомпозиция кода происходит гораздо более естественно.

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

Как ФП улучшает программиста





Далее мне хотелось бы поделиться своим опытом, не относящимся непосредственно к написанию программ на функциональных языках, и рассказать про то, чем ещё знание и использование ФП было полезно для меня и может оказаться полезным для вас:

  1. Изучение альтернативной парадигмы само по себе полезно для мозга, поскольку в процессе освоения программирования в функциональном стиле вы научитесь смотреть на привычные вещи по-другому. Кому-то такой способ мышления покажется гораздо более естественным, чем императивный. Можно долго спорить о том, что нужно и не нужно в индустриальном программировании, но там в любом случае нужны хорошие мозги, а их нужно тренировать. Осваивайте то, что не используете в работе: Лиспы, Пролог, Haskell, Brainfuck, Piet. Это поможет расширить кругозор и может стать для вас увлекательной головоломкой. Со временем вы сможете начать применять более элегантные решения в функциональном стиле, даже если пишете на императивном языке.
  2. За функциональными языками стоит серьёзная теория, которая тоже может стать частью ваших увлечений или даже исследований, если вы в той или иной степени хотите связать свою жизнь с computer science. Учиться никогда не поздно, особенно когда перед вами будут наглядные примеры использования довольно занятной теории для решения повседневных задач. Я бы никогда не подумала, что уже после окончания университета буду смотреть лекции по теории категорий, которую мой мозг когда-то отказывался воспринимать, и решать задачки из курса ручкой на бумаге, просто потому что мне это интересно.
  3. Помимо расширения кругозора увлечение ФП поможет вам расширить и круг общения. Возможно, за тусовкой функциональщиков закрепилась репутация академических снобов, которые спрашивают у вас определение монады перед тем, как продолжить общение. Я тоже так раньше думала, пока меня не вытащили на первые функциональные митапы и конференции. Я была совсем неопытным джуном и не знала определение монады, но не встретила по отношению к себе никакого негатива. Напротив, я познакомилась с интересными людьми, увлечёнными своим делом и готовыми делиться опытом, рассказывать и объяснять. Разумеется, в любом комьюнити есть совершенно разные люди, кто-то вам понравится больше, кто-то покажется токсичным и отталкивающим, и это совершенно нормально. Гораздо важнее то, что у вас появится возможность обмениваться идеями с теми, кто смотрит на мир разработки немного иначе и обладает другим опытом.
  4. Самый неожиданный для меня пункт: мне было легче всего найти работу именно на Haskell! На данный момент мой опыт работы чуть больше пяти лет, за это время на двух из трёх местах работы я писала на Haskell, и это был наиболее комфортный и безболезненный опыт трудоустройства. Более того, начинала я тоже с позиции Haskell-разработчика, о чём ни разу не пожалела. На первой работе я получила базовые навыки клиент-серверной разработки и работы с БД. Мы занимались такими же приземлёнными и ненаучными задачами, как и компании, использующие более распространённые языки. На популярных сайтах с вакансиями вы, скорее всего, почти не найдёте ничего по запросу Haskell-разработчик. В лучшем случае найдутся вакансии, где указано, что знание альтернативных парадигм будет преимуществом. Однако, это не значит, что таких вакансий нет. В Твиттере и тематических каналах в Телеграме вакансии появляются регулярно. Да, их мало, нужно знать, где искать, но и хороших специалистов такого узкого профиля тоже немного. Разумеется, вас не возьмут сразу и везде, но свою востребованность вы почувствуете значительно сильнее, чем при поиске работы на более распространённых языках. Возможно, компании могут быть готовы вкладываться в развитие программистов в нужном направлении: не можешь найти хаскелиста вырасти его сам!

Заключение


Появление элементов ФП в популярных языках индустриальной разработки, таких как Python, C++, Kotlin, Swift и т.д., подтверждает, что этот подход действительно полезен и обладает сильными сторонами. Применение функционального стиля позволяет получить более надёжный код, который проще разбивать на части, обобщать и тестировать, независимо от языка программирования. Разумеется, функциональный язык позволяет использовать все перечисленные преимущества по максимуму, предоставляя естественные конструкции с высокой степенью выразительности.

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

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

Перевод Optional.stream()

08.06.2021 20:05:53 | Автор: admin

На этой неделе я узнал об одной интересной "новой" возможности Optional, о которой хочу рассказать в этом посте. Она доступна с Java 9, так что новизна ее относительна.

Давайте начнем со следующей последовательности для вычисления общей цены заказа:

public BigDecimal getOrderPrice(Long orderId) {    List<OrderLine> lines = orderRepository.findByOrderId(orderId);    BigDecimal price = BigDecimal.ZERO;           for (OrderLine line : lines) {        price = price.add(line.getPrice());       }    return price;}
  • Предоставьте переменную-аккумулятор для цены

  • Добавьте цену каждой строки к общей цене

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

public BigDecimal getOrderPrice(Long orderId) {    List<OrderLine> lines = orderRepository.findByOrderId(orderId);    return lines.stream()                .map(OrderLine::getPrice)                .reduce(BigDecimal.ZERO, BigDecimal::add);}

Давайте сосредоточимся на переменной orderId : она может содержать null.

Императивный способ обработки nullзаключается в том, чтобы проверить его в начале метода - и в конечном итоге сбросить:

public BigDecimal getOrderPrice(Long orderId) {    if (orderId == null) {        throw new IllegalArgumentException("Order ID cannot be null");    }    List<OrderLine> lines = orderRepository.findByOrderId(orderId);    return lines.stream()                .map(OrderLine::getPrice)                .reduce(BigDecimal.ZERO, BigDecimal::add);}

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

public BigDecimal getOrderPrice(Long orderId) {    return Optional.ofNullable(orderId)                                        .map(orderRepository::findByOrderId)                               .flatMap(lines -> {                                                    BigDecimal sum = lines.stream()                        .map(OrderLine::getPrice)                        .reduce(BigDecimal.ZERO, BigDecimal::add);                return Optional.of(sum);                                       }).orElse(BigDecimal.ZERO);                            }
  1. Оберните orderId в Optional

  2. Найдите соответствующие строки заказа

  3. Используйте flatMap(), чтобы получить Optional<BigDecimal>; map() получит Optional<Optional<BigDecimal>>

  4. Нам нужно обернуть результат в Optional, чтобы он соответствовал сигнатуре метода.

  5. Если Optional не содержит значения, сумма равна 0

Optional делает код менее читабельным! Я считаю, что понятность должна быть всегда важнее стиля кода.

К счастью, Optional предлагает метод stream() (начиная с Java 9). Он позволяет упростить функциональный конвейер:

public BigDecimal getOrderPrice(Long orderId) {    return Optional.ofNullable(orderId)            .stream()            .map(orderRepository::findByOrderId)            .flatMap(Collection::stream)            .map(OrderLine::getPrice)            .reduce(BigDecimal.ZERO, BigDecimal::add);}

Вот краткая информация о типе на каждой строке:

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


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

Подробнее..

Изучаю Scala Часть 2 Todo лист с возможностью загрузки картинок

28.06.2020 22:13:05 | Автор: admin

Привет Хабр! Следующих этап изучения нового языка это старый добрый todo list только не простой а с загрузкой и скачиванием картинок чтобы научится работе с базой данных и файловой системой. За подробностями добро пожаловать под кат.

Содержание




Ссылки


Исходники
Образы docker image

API


Описание апи в swagger и эндпойнты я сделал с помощью Tapir. Он позволяет своим DSL описать API которое мы хотим реализовать.
  def withStatus[A](f: IO[A]): IO[Either[(StatusCode, String), A]] =    f.attempt.map(x => x match {      case Right(value) => Right(value)      case Left(value) => Left(StatusCode.InternalServerError, value.getMessage)    })val baseEndpoint = endpoint    .in("api" / "v1")    .errorOut(statusCode.and(stringBody))private val baseImageEndpoint = baseEndpoint    .in("images")    .tag("Images")  private val download = baseImageEndpoint    .summary("Скачать картинку")    .description("Скачивает картику по ее идентификатору")    .get    .in(path[Long]("id"))    .out(header[Long](HeaderNames.ContentLength))    .out(streamBody[Stream[IO, Byte]](schemaFor[File], CodecFormat.OctetStream()))    .serverLogic(x => withStatus(imagesService.download(x)))


на основе коллекции таких эндпойнтов создаются роуты, а на основе них документация Swagger
    endpoints = todosController.endpoints ::: imagesController.endpoints    routes = endpoints.toRoutes;    docs = endpoints.toOpenAPI("The Scala Todo List", "0.0.1")    yml: String = docs.toYaml    appRoutes = routes <+> new SwaggerHttp4s(yml, "swagger").routes[IO]


Server


В качестве сервера Tapir поддерживает несколько бекендов. Я использовал http4s
    httpApp = Router(      "/" -> appRoutes    ).orNotFound    blazeServer <- BlazeServerBuilder[IO](serverEc)      .bindHttp(settings.host.port, settings.host.host)      .withHttpApp(httpApp)      .resource


Работа с файлами и стримы


Для работы с файлами я использовал стримы из fs2
import fs2.{Stream, io}  def get(path: Path): Stream[IO, Byte] =    io.file.readAll[IO](path, blocker, 4096)


Работа с базой данных


Для работы с БД я использовал doobie и он мне чертовски понравился потому что напомнил старый добрый Dapper ORM. Позволяет маппить DTO и выполнять SQL запросы.
  def add(image: Image): IO[Long] = sql"""         INSERT INTO images (hash, file_path)         VALUES (${image.hash}, ${image.filePath})""".update    .withUniqueGeneratedKeys[Long]("id")    .transact(xa)


Сборка и упаковка в образ Docker


Я захотел собрать все в один единственный файл как например делает это Go или .NET Core с нужными настройками поэтому использовал sbt-native-packager и плагин к нему sbt-assembly. Собранный файл можно запустить с помощью команды
java -jar <имя файла>

Потом сделал DockerFile для запуска этого образа в контейнере
FROM hseeberger/scala-sbt:11.0.2-oraclelinux7_1.3.12_2.13.3 AS baseCOPY . /rootWORKDIR /rootRUN sbt universal:packageZipTarballRUN sbt testFROM openjdk:15-alpine as finalCOPY --from=base /root/target/scala-2.13/scala-todo-api.jar /rootWORKDIR /rootEXPOSE 8080ENTRYPOINT ["java","-jar","scala-todo-api.jar"]

Собранный образ автоматом отправляется в Registry гитлаба через его встроенный CI/CD

Настройки


Настройки сервера загружаю с помощью библиотеки PureConfig и потом так как я использую Docker дополняю их из переменных окружения. Файл application.conf:
db {  url = "jdbc:postgresql://localhost:5432/todos_db"  url = ${?TODO_API_DB_URL}  user = "postgres"  user = ${?TODO_API_DB_USER}  password = "postgres"  password = ${?TODO_API_DB_PASSWORD}}host {  port = 8080  port = ${?TODO_API_HOSTING_PORT}  host = "0.0.0.0"  host = ${?TODO_API_HOSTING_HOST}}


val config = ConfigSource.default.load[AppSettings]
Подробнее..

Изучаю Scala Часть 4 WebSocket

30.08.2020 02:18:14 | Автор: admin

Привет, Хабр! На этот раз я по пробовал сделать простенький чат через ВебСокеты. За подробностями добро пожаловать под кат.

Содержание



Ссылки


  1. Исходники
  2. Образы docker image
  3. Tapir
  4. Http4s
  5. Fs2
  6. Doobie
  7. ScalaTest
  8. ScalaCheck
  9. ScalaTestPlusScalaCheck


Собственно весь код находиться в одном объект ChatHub
class ChatHub[F[_]] private(                             val topic: Topic[F, WebSocketFrame],                             private val ref: Ref[F, Int]                           )                           (                             implicit concurrent: Concurrent[F],                             timer: Timer[F]                           ) extends Http4sDsl[F] {  val endpointWs: ServerEndpoint[String, Unit, String, Stream[IO, WebSocketFrame], IO] = endpoint    .get    .in("chat")    .tag("WebSockets")    .summary("Подключится к общему чату. Например по такому адресу: ws://localhost:8080/chat")    .description("Подключает к общему чату")    .in(      stringBody        .description("Сообщение которое будет отправлено пользователям в чате")        .example("Привет!")    )    .out(      stringBody        .description("Сообщение которое кто-то написал в чат")        .example("6 : Сообщение от клиента с Id подключения f518a53d: Привет!")    )    //Заглушка которая всегда отвечает ошибкой.     .serverLogic(_ => IO(Left(()): Either[Unit, String]))  def routeWs: HttpRoutes[F] = {    HttpRoutes.of[F] {      case GET -> Root / "chat" => logic()    }  }  private def logic(): F[Response[F]] = {    val toClient: Stream[F, WebSocketFrame] =      topic.subscribe(1000)    val fromClient: Pipe[F, WebSocketFrame, Unit] =      handle    WebSocketBuilder[F].build(toClient, fromClient)  }  private def handle(s: Stream[F, WebSocketFrame]): Stream[F, Unit] = s    .collect({      case WebSocketFrame.Text(text, _) => text    })    .evalMap(text => ref.modify(count => (count + 1, WebSocketFrame.Text(s"${count + 1} : $text"))))    .through(topic.publish)}object ChatHub {  def apply[F[_]]()(implicit concurrent: Concurrent[F], timer: Timer[F]): F[ChatHub[F]] = for {    ref <- Ref.of[F, Int](0)    topic <- Topic[F, WebSocketFrame](WebSocketFrame.Text("==="))  } yield new ChatHub(topic, ref)}

Тут надо сразу сказать про Topic примитив синхронизации из Fs2 который позволяет сделать модель Publisher Subscriber причем у вас может быть много Publisher и одновременно много Subscriber. Вообще в него лучшее отправлять сообщения через какой-то буфер вроде Queue потому что у него есть ограничения на количество сообщения в очереди и Publisher ждет пока все Subscriber не получат сообщения в свою очередь сообщений и если она переполнена то может и зависнуть.
val topic: Topic[F, WebSocketFrame],

Тут еще я считаю количество сообщений которые были переданы в чат как номер каждого сообщения. Так как это мне нужно делать из разных потоков я использовал аналог Atomic который тут называется Ref и гарантирует атомарность операции.
  private val ref: Ref[F, Int]

Обработка потока сообщений от пользователей.
  private def handle(stream: Stream[F, WebSocketFrame]): Stream[F, Unit] =     stream//Достаем из фрейма текстовое сообщение и фильтруем фреймы.     .collect({      case WebSocketFrame.Text(text, _) => text    })//Атомарно увеличиваем наш счетчик с сохранением нового значения и добавления его значения к тексту сообщения пользователя.    .evalMap(text => ref.modify(count => (count + 1, WebSocketFrame.Text(s"${count + 1} : $text"))))//Каждое пришедшее сообщение отправляем в топик    .through(topic.publish)

Собственно сама логика создания сокета.
private def logic(): F[Response[F]] = {//Откуда получать данные для клиента.    val toClient: Stream[F, WebSocketFrame] =//Просто подписываемся на данные которые будут приходить в топик      topic.subscribe(1000)//Что будем делать с данными которые приходить от клиента    val fromClient: Pipe[F, WebSocketFrame, Unit] =//Просто отправляем данные в топик после обработки      handle//Создаем веб сокет с созданными ранее генератором и потребителем данных.    WebSocketBuilder[F].build(toClient, fromClient)  }

Связываем наш сокет с роутом на сервере (ws://localhost:8080/chat)
def routeWs: HttpRoutes[F] = {    HttpRoutes.of[F] {      case GET -> Root / "chat" => logic()    }  }

Собственно на этом все. Дальше уже можно запускать сервер с этим роутом. Мне еще захотелось какую ни какую документацию сделать. Вообще для документирования WebSocket и прочего основанного на событиях взаимодействия вроде RabbitMQ AMPQ есть AsynAPI но под Tapir там нет ничего поэтому просто сделал для Swagger описание эндпойнта как GET запрос. Работать он конечно не будет. Точнее 501 ошибку будет возвращать зато будет отображаться в Swagger
  val endpointWs: Endpoint[String, Unit, String, fs2.Stream[F, Byte]] = endpoint    .get    .in("chat")    .tag("WebSockets")    .summary("Подключится к общему чату. Например по такому адресу: ws://localhost:8080/chat")    .description("Подключает к общему чату")    .in(      stringBody        .description("Сообщение которое будет отправлено пользователям в чате")        .example("Привет!")    )    .out(      stringBody        .description("Сообщение которое кто-то написал в чат")        .example("6 : Сообщение от клиента с Id подключения f518a53d: Привет!")    )

В самом сваггере это выглядит вот так

Подключаем наш чат к нашему серверу API
    todosController = new TodosController()    imagesController = new ImagesController()//Создаем объект нашего чата    chatHub <- Resource.liftF(ChatHub[IO]())    endpoints = todosController.endpoints ::: imagesController.endpoints//Добавляем его эндпойнт в документацию Swagger    docs = (chatHub.endpointWs :: endpoints).toOpenAPI("The Scala Todo List", "0.0.1")    yml: String = docs.toYaml//Добавляем его маршрут в список маршрутов приложения    routes = chatHub.routeWs <+>      endpoints.toRoutes <+>      new SwaggerHttp4s(yml, "swagger").routes[IO]    httpApp = Router(      "/" -> routes    ).orNotFound    blazeServer <- BlazeServerBuilder[IO](serverEc)      .bindHttp(settings.host.port, settings.host.host)      .withHttpApp(httpApp)      .resource

Подключаемся к чату крайне простым скриптом.
    <script>        const id = `f${(~~(Math.random() * 1e8)).toString(16)}`;        const webSocket = new WebSocket('ws://localhost:8080/chat');        webSocket.onopen = event => {            alert('onopen ');        };        webSocket.onmessage = event => {            console.log(event);            receive(event.data);        };        webSocket.onclose = event => {            alert('onclose ');        };        function send() {            let text = document.getElementById("message");            webSocket.send(`Сообщение от клиента с Id подключения ${id}: ${text.value}`);            text.value = '';        }        function receive(m) {            let text = document.getElementById("chat");            text.value = text.value + '\n\r' + m;        }    </script>

На этом собственно все. Надеюсь кому-то кто тоже изучает скала будет интересна эта статья а может даже полезна.
Подробнее..

Изучаю Scala Часть 5 Http Requests

09.12.2020 04:19:27 | Автор: admin
Привет хабр! Продолжаю изучать Scala. Большинство бекендов так или иначе интегрированы с другими и делают HTTP запросы. Так как я на стек Cats и http4s ориентирован то буду рассматривать и изучать именно его. Сделаю запросы с куками, телом в json и в form, c файлом, с хедерами. Тут Hirrolot мне скорее всего минус поставит. Хочу сказать что может быть кому-то кто тоже изучает Scala будет полезна эта статья. Да и меня написание таких статей мотивирует изучать дальше. Люблю тебя малой. Расти большой не будь лапшой. Я уверен из тебя получится просто отличный инженер или даже может быть ученый в области IT. Давненько меня тут не было. В общем штормило у меня на личном фронте. С начала мы встречались обнимались и целовались с Марго. Потом мы расстались. Потом я переживал из-за этого. Потом работы навалилось. Вот так примерно у меня последние месяцы прошли. Взгрустнул, выпил и решил я написать сюда. И так, начнем.

Содержание



Ссылки


  1. Исходники
  2. Образы docker image
  3. Tapir
  4. Http4s
  5. Fs2
  6. Doobie
  7. ScalaTest
  8. ScalaCheck
  9. ScalaTestPlusScalaCheck


Тестовый контроллер который будет отвечать на наши запросы:
import cats.effect.{ContextShift, IO}import domain.todos.entities.Todoimport io.circe.generic.auto._import sttp.model.CookieWithMetaimport sttp.tapir.json.circe.jsonBodyimport sttp.tapir.{header, _}class TestController(implicit contextShift: ContextShift[IO]) extends ControllerBase {  private val baseTestEndpoint = baseEndpoint    .in("test")    .tag("Test")//Сюда мы будем делать наш запрос  private val postTest = baseTestEndpoint    .summary("Тестовый эндпойнт для запроска к самому себе")    .description("Возвращает тестовые данные")    .post    .in(header[String]("test_header"))    .in(jsonBody[List[Todo]])    .in(cookies)    .out(header[String]("test_header_out"))    .out(jsonBody[List[Todo]])    .out(setCookies)    .serverLogic(x => withStatus(IO {      (x._1 + x._3.map(c => c.name + "" + c.value).fold("")((a, b) => a + " " + b), x._2, List(CookieWithMeta(name = "test", value = "test_value")))    }))//Этот метод будет запускать наш запрос  private val runHttpRequestTes = baseTestEndpoint    .summary("Запускает тестовый запрос к самому себе")    .description("Запускает тестовый запрос к самому себе")    .get    .out(stringBody)    .serverLogic(_ => withStatus(runHttp()))  def runHttp(): IO[String] = {    ClientExamples.execute().as("Ok")  }  val endpoints = List(    postTest,    runHttpRequestTes  )}


Собственно сам запрос:
import cats.effect.{ContextShift, IO}import com.typesafe.scalalogging.StrictLoggingimport domain.todos.entities.Todoimport io.circe.generic.auto._import org.http4s.circe.CirceEntityCodec.circeEntityEncoderimport org.http4s.client.blaze._import org.http4s.client.middleware.Loggerimport org.http4s.headers._import org.http4s.{MediaType, Uri, _}import org.log4s._import java.time.Instantimport scala.concurrent.ExecutionContext.globalobject ClientExamples extends StrictLogging {  private[this] val logger = getLogger  def execute()(implicit contextShift: ContextShift[IO]) = {//Создаем клиент    BlazeClientBuilder[IO](global).resource.use { client =>      logger.warn("Start Request")//Оборачиваем его в мидлвар который будет логгировать запросы и ответы. //Указываем логгировать и боди и хедеры      val loggedClient = Logger[IO](true, true)(client)//Парсим адресс и небезопасным методом достаем результат      val uri = Uri.fromString("http://localhost:8080/api/v1/test").toOption.get//Создаем запрос. Указываем что это будет POST запрос по адресу что мы сформировали ранее      val request: Request[IO] = Request[IO](method = Method.POST, uri = uri)//Указываем что в json теле запроса передавать массив todo        .withEntity(List(Todo(1, "Test", 2, Instant.now())))//Указываем заголовки которые будут у запроса. Тут один наш кастомный.        .withHeaders(Accept(MediaType.application.json), Header(name = "test_header", value = "test_header_value"))//Указываем что с запросом будут оправляться куки с таким значением        .addCookie("test_cookie", "test_cookie_value")//Выполняем запрос      loggedClient.run(request).use(r => {        logger.warn("End Request")//Логгируем статус (200, 404, 500 и т.д)        logger.warn(r.status.toString())//Логгируем ответ        logger.warn(r.toString())//Пишем в логи хедеры ответа. Там в том числе есть Set-Cookie        logger.warn(r.headers.toString())//bodyText возвращает Stream[IO,String] и мы логгируем данные в нем//Можно десериализовать из этого json ответ сервера.        r.bodyText.map(t =>  logger.warn(t)).compile.drain      })    }  }}


В результате в логах увидим такой текст:
//Наш запрос 02:54:44.634 [ioapp-compute-7] INFO org.http4s.client.middleware.RequestLogger - HTTP/1.1 POST http://localhost:8080/api/v1/test Headers(Accept: application/json, test_header: test_header_value, Cookie: <REDACTED>) body="[{"id":1,"name":"Test","imageId":2,"created":"2020-12-08T23:54:44.627434500Z"}]"//Наш статус 02:54:44.641 [scala-execution-context-global-62] WARN appServices.ClientExamples - 200 OK//Наш ответ02:54:44.641 [scala-execution-context-global-62] WARN appServices.ClientExamples - Response(status=200, headers=Headers(test_header_out: test_header_value test_cookietest_cookie_value, Set-Cookie: <REDACTED>, Content-Type: application/json, Date: Tue, 08 Dec 2020 23:54:44 GMT, Content-Length: 79))//Хедеры нашего ответа02:54:44.641 [scala-execution-context-global-62] WARN appServices.ClientExamples - Headers(test_header_out: test_header_value test_cookietest_cookie_value, Set-Cookie: test=test_value, Content-Type: application/json, Date: Tue, 08 Dec 2020 23:54:44 GMT, Content-Length: 79)//Тело (json) нашего ответа сервера02:54:44.643 [ioapp-compute-6] WARN appServices.ClientExamples - [{"id":1,"name":"Test","imageId":2,"created":"2020-12-08T23:54:44.627434500Z"}]


Тут был специально показан запрос в максимально общем виде. Показано как установить куки, хедеры, тело запроса. Если нужно данные формы отправить или там файл то есть для этого пару способов. Сами данные методом .withEntity выставляются а вот объект формируется по другому
//Тут можно файл отправить через Part.fileData val data = Multipart(parts = Vector(Part.formData("age","18"):Part[IO]))//Или val data= UrlForm(("age","18"))//И создаем запрос  val request: Request[IO] = Request[IO](method = Method.POST, uri = uri)        .withEntity(data)
Подробнее..

Из песочницы Функциональное программирование на Python для самых маленьких Часть 1 Lambda Функция

22.06.2020 10:10:48 | Автор: admin
image

Я решил написать эту серию статей ибо считаю что никто не должен сталкиваться с той стеной непонимания с которой столкнулся я где для того чтобы понять что то в Функциональном Программировании (Далее ФП) тебе надо уже знать многое в ФП. Эту статью я старался написать максимально просто настолько понятно чтобы ее суть мог уловить мой племянник школьник который сейчас делает свои первые шаги в Python

Небольшое введение


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

  • Чистая Функция
  • Функции высшего порядка

Но для понимания того что сказано в этой статье знать это не обязательно.

Итак, начнем


Для начала надо понять следующее что такое Функциональное Программирование вообще. Лично я знаю две самые часто упоминаемые парадигмы в повседневном программировании это ООП и ФП.

Если упрощать совсем и объяснять на пальцах то описать эти две парадигмы можно следующим образом:

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

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

Это относится и к ФП взял какие то данные, взял какую то функцию, поигрался с ними и выдал что то на выходе.

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

В большинстве своем ФП (как я его воспринимаю) это просто упрощенное написание кода. Любой код написанный в функциональном стиле может быть довольно легко переписан в обычном без потери качества но более примитивно. Цель ФП заключается в том чтобы писать код более простой, понятный и который легче поддерживать а также который занимает меньше памяти ну и куда же без этого разумеется главная новая мораль программирования DRY (Dont Repeat Yourself Не повторяйся).

Сейчас мы с вами разберем одну из основных функций которые применяются в ФП Lambda функцию.

В следующих статьях мы разберем такие функции как Map, Zip, Filter и Reduce.

Lambda функция


Lambda это инструмент в python и других языках программирования для вызова анонимных функций. Многим это скорее всего ничего не скажет и никак не прояснит того как она работает поэтому я расскажу вам просто механизм работы lambda выражений.

Все очень просто.

Рассмотрим пример. Например нам надо написать функцию которая бы считала площадь круга при известном радиусе.

Формула площади круга это

S = pi*(r**2)

где
S это площадь круга
pi математическая константа равная 3.14 которую мы получим из стандартной библиотеки Math
r радиус круга единственная переменная которую мы будем передавать нашей функции

Круг с радиусом

Теперь оформим это все в python

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно# Пишем функцию которая будет вычислять площадь круга по заданному радиусу в обычном варианте записиdef area_of_circle_simple(radius):  return pi_const*(radius**2)print(area_of_circle_simple(5))print(area_of_circle_simple(12))print(area_of_circle_simple(26))>>>78.5>>>452.16>>>2122.64

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

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно# Пишем функцию которая будет вычислять площадь круга по заданному радиусу в функциональном стилеarea_of_circle_by_lambda = lambda radius: pi_const*(radius**2) print(area_of_circle_by_lambda(5))print(area_of_circle_by_lambda(12))print(area_of_circle_by_lambda(26))>>>78.5>>>452.16>>>2122.64

Для полноты картины покажем как вызывать lambda функцию анонимно

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака # после запятой иначе она будет выглядеть # как 3.141592653589793 а нам это будет неудобноprint((lambda radius: pi_const*(radius**2))(5))print((lambda radius: pi_const*(radius**2))(12))print((lambda radius: pi_const*(radius**2))(26))>>>78.5>>>452.16>>>2122.64

Что бы вы понимали анонимный вызов функции подразумевает то что вы используете её нигде не объявляя как в примере выше.

Лямбда функция работает по следующему принципу

func = lambda перечисляются аргументы через запятую : что то с ними делаетсяprint(func(передаем аргументы))>>>получаем результат того что находится после двоеточия двумя строками выше

Рассмотрим пример с двумя входными аргументами. Например нам надо посчитать объем конуса по следующей формуле:

V = (height*pi_const*(radius**2))/3

Конус с габаритами

Запишем это все в python:

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно#Формула объема конуса в классической форме записиdef cone_volume(height, radius):  volume = (height*pi_const*(radius**2))/3  return volumeprint(cone_volume(3, 10))>>>314.0

А теперь как это будет выглядеть в lambda форме:

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно#Формула объема конуса в лямбда записиcone_volume = lambda height, radius : (height*pi_const*(radius**2))/3print(cone_volume(3, 10))>>>314.0

Вообще количество переменных никак не ограничено. Для примера посчитаем объем усеченного конуса где у нас учитываются 3 разные переменные.

Объем усеченного конуса считается по формуле:

V = (pi_const*height*(r1**2 + r1*r2 + r2**2))/3

Усеченный конус с габаритами

И вот как это будет выглядеть в python классически

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно#Формула объема усеченного конуса в классической записиdef cone_volume(h,r1,r2):  return (pi_const * h * (r1 ** 2 + r1 * r2 + r2 ** 2))/3print(cone_volume(12, 8, 5))print(cone_volume(15, 10, 6))print(cone_volume(20, 12, 9))>>>1620.24>>>3077.20>>>6970.8

А теперь покажем как это будет выглядеть в lambda но при этом не будем объявлять функцию заранее а опишем ее в момент вывода:

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобноprint((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(12, 8, 5))print((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(15, 10, 6))print((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(20, 12, 9))>>>1620.24>>>3077.20>>>6970.8

После того как мы разобрались как работает lambda функция давайте разберем еще кое что интересное что можно делать с помощью lambda функции что может оказаться для вас весьма неожиданным Сортировку

Сортировать одномерные списки в python с помощью lambda довольно глупо это будет выглядеть как бряцание мускулами там где оно совсем не нужно
Ну серьезно допустим у нас есть обычный список (не важно состоящий из строк или чисел) и нам надо его отсортировать тут же проще всего использовать встроенную функцию sorted()
И в правду, давайте посмотрим на это

new_int_list = [43,23,56,75,12,32] # Создаем список чиселprint(sorted(new_int_list)) # Сортируем список чиселnew_string_list = ['zum6z', 'yybt0', 'h1uwq', '2k9f9', 'hin9h', 'b0p0m'] # Создаем список строкprint(sorted(new_string_list)) # Сортируем список строк>>>[12, 23, 32, 43, 56, 75]>>>['2k9f9', 'b0p0m', 'h1uwq', 'hin9h', 'yybt0', 'zum6z']

В таких ситуациях действительно хватает обычного sorted() (ну или sort() если вам нужно изменить текущий список на месте без создания нового изменив исходный)

Но что если нужно отсортировать список словарей по разным ключам? Тут может быть запись как в классическом стиле так и в функциональном. Допустим у нас есть список книг вселенной Песни Льда и Пламени с датами их публикаций и количеством страниц в них.
Как всегда начнем с классической записи.

# Создали список из словарей книгasoiaf_books = [  {'title' : 'Game of Thrones', 'published' : '1996-08-01', 'pages': 694},  {'title' : 'Clash of Kings', 'published' : '1998-11-16', 'pages': 761},  {'title' : 'Storm of Swords', 'published' : '2000-08-08', 'pages': 973},  {'title' : 'Feast for Crows', 'published' : '2005-10-17', 'pages': 753},  {'title' : 'Dance with Dragons', 'published' : '2011-07-12', 'pages': 1016}]# Функция по получению названия книгиdef get_title(book):    return book.get('title')# Функция по получению даты публикации книгиdef get_publish_date(book):    return book.get('published')# Функция по получению количества страниц в книгеdef get_pages(book):    return book.get('pages')# Сортируем по названиюasoiaf_books.sort(key=get_title)for book in asoiaf_books:  print(book)print('-------------')# Сортируем по датамasoiaf_books.sort(key=get_publish_date)for book in asoiaf_books:  print(book)print('-------------')# Сортируем по количеству страницasoiaf_books.sort(key=get_pages)for book in asoiaf_books:  print(book)>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}

А теперь перепишем это все через lambda функцию

# Создали список из словарей книгasoiaf_books = [  {'title' : 'Game of Thrones', 'published' : '1996-08-01', 'pages': 694},  {'title' : 'Clash of Kings', 'published' : '1998-11-16', 'pages': 761},  {'title' : 'Storm of Swords', 'published' : '2000-08-08', 'pages': 973},  {'title' : 'Feast for Crows', 'published' : '2005-10-17', 'pages': 753},  {'title' : 'Dance with Dragons', 'published' : '2011-07-12', 'pages': 1016}]# Сортируем по названиюasoiaf_books.sort(key=lambda book: book.get('title'))for book in asoiaf_books:  print(book)print('-------------')# Сортируем по датамasoiaf_books.sort(key=lambda book: book.get('published'))for book in asoiaf_books:  print(book)print('-------------')# Сортируем по количеству страницasoiaf_books.sort(key=lambda book: book.get('pages'))for book in asoiaf_books:  print(book)>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}

Таким образом lambda функция хорошо подходит для сортировки многомерных списков по разным параметрам.

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

Но где же тут та самая экономия места, времени и памяти? Экономится максимум пара строк.

И вот тут мы подходим к реально интересным вещам.

Которые разберем в следующей статье где мы обсудим map функцию.
Подробнее..

Composable Architecture свежий взгляд на архитектуру приложения. Тесты

16.11.2020 12:07:26 | Автор: admin

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


В прошлой серии


Часть 1 основные компоненты архитектуры и как работает Composable Architecture


Тестируемый код


В предыдущем выпуске был разработан каркас приложения список покупок на Composable Architecture. Перед тем как продолжить наращивать функционал необходимо сохраниться покрыть код тестами. В этой статье рассмотрим два вида тестов: unit тесты на систему и snapshot тесты на UI.


Что мы имеем?


Еще раз взглянем на текущее решение:


  • состояние экрана описывается списком продуктов;
  • два вида событий: изменить продукт по индексу и добавить новый;
  • механизм, обрабатывающий действия и меняющий состояние системы яркий претендент для написания тестов.

struct ShoppingListState: Equatable {    var products: [Product] = []}enum ShoppingListAction {    case productAction(Int, ProductAction)    case addProduct}let shoppingListReducer: Reducer<ShoppingListState, ShoppingListAction, ShoppingListEnviroment> = .combine(    productReducer.forEach(        state: \.products,        action: /ShoppingListAction.productAction,        environment: { _ in ProductEnviroment() }    ),    Reducer { state, action, env in        switch action {        case .addProduct:            state.products.insert(                Product(id: UUID(), name: "", isInBox: false),                at: 0            )            return .none        case .productAction:            return .none        }    })

Типы тестов


Как понять что архитектура не очень? Легко, если вы не можете покрыть ее на 100% тестами (Vladislav Zhukov)

Не все архитектурные паттерны четко регламентируют подходы к тестированию. Рассмотрим как эту задачу решает Composable Arhitecutre.


Unit тесты


Одна из причин полюбить Composable Arhitecutre является подход к написанию unit тестов.


image


Тестирование основного механизма системы recuder'а происходит с помощью построения цепочки шагов: send(Action) и receive(Action). На каждом этапе проверяем, что состояние системы изменилось должным образом.


Send(Action) позволяет имитировать действий пользователя.


Receive(Action) говорит о том, что на предыдущем шаге выполнился эффект и вернул результат action.


В конце теста или по ходу цепочки в блоке .do {} проверяем обращения к сервисам.


Первый наш тест посвящен операции добавления продукта.


func testAddProduct() {    // Создаем тестовый стор    let store = TestStore(        initialState: ShoppingListState(            products: []        ),        reducer: shoppingListReducer,        environment: ShoppingListEnviroment()    )    // описываем ожидаемое поведение системы    store.assert(        // создаем событие добавление продукта        .send(.addProduct) { state in            // описываем ожидаемое состояние системы            state.products = [                Product(                    id: UUID(),                    name: "",                    isInBox: false                )            ]        }    )}

Первое на что следует обратить внимание, что тестирование системы происходит независимо от слоя представления.


image


Запускаем тест и ловим фейл


Достаточно информативное сообщение об ошибке говорит нам о несовпадении присвоенного идентификатора продукта с ожидаемым:


image


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


Reducer чистая функция


Что же такое чистая функция?


Чистые функции это любые функции, исходные данные которых получены исключительно из их входных данных и не вызывают побочных эффектов в приложении.


В действительности, внутри нашей функции генерируется UUID в качестве идентификатора продукта. Такое поведение называется побочным эффектом, а наша функция становится "грязной".


Чтобы это исправить необходимо генерировать UUID через сервис. В Composable Architecture сервисы представлены объектом окружения (Environment).


Добавим в наш ShoppingListEnviroment сервис (функцию) генерации UUID.


struct ShoppingListEnviroment {    var uuidGenerator: () -> UUID}

И используем ее при создании продукта:


Reducer { state, action, env in    switch action {    case .addProduct:        state.products.insert(            Product(                id: env.uuidGenerator(),                name: "",                isInBox: false            ),            at: 0        )        return .none    ...    }}

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


func testAddProduct() {    let store = TestStore(        initialState: ShoppingListState(),        reducer: shoppingListReducer,        // Создаем окружение        environment: ShoppingListEnviroment(            // инжектим сервис генерации мокового UUID            uuidGenerator: { UUID(uuidString: "00000000-0000-0000-0000-000000000000")! }        )    )    store.assert(        // Имитируем нажатие на кнопку "добавить продукт"        .send(.addProduct) { newState in            // Описываем ожидаемое изменение состояния системы            newState.products = [                Product(                    // продукту установился определенный в сервисе UUID                    id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,                    name: "",                    isInBox: false                )            ]        }    )}

Чтобы посмотреть на более интересный тест, добавим кэширование списка продуктов из следующего выпуска. Для этого добавим еще два сервиса: saveProducts и loadProducts:


struct ShoppingListEnviroment {    var uuidGenerator: () -> UUID    var save: ([Product]) -> Effect<Never, Never>    var load: () -> Effect<[Product], Never>}

Предполагая, что операции загрузки и сохранения могут быть асинхронные, они возвращают Effect. Effect не что иное как Publisher. Более подробнее рассмотрим в следующей серии.


Пишем тест:


func testAddProduct() {    // проверим, что сохраняется то, что нужно    var savedProducts: [Product] = []    // убедимся, что количество сохранений совпадает с ожидаемым    var numberOfSaves = 0    // создаем тестовый стор    let store = TestStore(        initialState: ShoppingListState(products: []),        reducer: shoppingListReducer,        environment: ShoppingListEnviroment(            uuidGenerator: { .mock },            // функция сохранения принимает массив продуктов            // и возвращает эффект сохраняющий список            saveProducts: { products in Effect.fireAndForget { savedProducts = products; numberOfSaves += 1 } },            // функция загрузки списка            // возвращает эффект с закэшированным списком             loadProducts: { Effect(value: [Product(id: .mock, name: "Milk", isInBox: false)]) }        )    )    store.assert(        // иммитируем отправку события load при показе view        .send(.loadProducts),        // событие load запускает эффект загрузки данных        // который возвращает событие productsLoaded([Product])        .receive(.productsLoaded([Product(id: .mock, name: "Milk", isInBox: false)])) {            $0.products = [                Product(id: .mock, name: "Milk", isInBox: false)            ]        },        // добавляем новый продукт в список        .send(.addProduct) {            $0.products = [                Product(id: .mock, name: "", isInBox: false),                Product(id: .mock, name: "Milk", isInBox: false)            ]        },        // ожидаем, что предыдущее действие вызывало эффект сохранения        .receive(.saveProducts),        // после выполнения эффекта проверяем сохраненный результат        .do {            XCTAssertEqual(savedProducts, [                Product(id: .mock, name: "", isInBox: false),                Product(id: .mock, name: "Milk", isInBox: false)            ])        },        // задаем имя добавленному продукту        .send(.productAction(0, .updateName("Banana"))) {            $0.products = [                Product(id: .mock, name: "Banana", isInBox: false),                Product(id: .mock, name: "Milk", isInBox: false)            ]        },        // имитируем событие сохранения в endEditing textFiled'a         .send(.saveProducts),        // после выполнения эффекта проверяем сохраненный результат        .do {            XCTAssertEqual(savedProducts, [                Product(id: .mock, name: "Banana", isInBox: false),                Product(id: .mock, name: "Milk", isInBox: false)            ])        }    )    // убеждаемся, что сохранение произошло только 2 раза    XCTAssertEqual(numberOfSaves, 2)}

В этом блоке мы:


  • рассмотрели написание unit тестов на систему;
  • определили инструменты тестирования;
  • написали тест, имитирующий действия пользователя при добавлении нового продукта в список.

Unit-Snapshot тесты на UI


Для snapshot тестов, авторы Composable Arhitecture разработали библиотеку SnapshotTesting (также можно использовать любое другое решение).


Для текущей итерации разработки, определяем минимальное количество различных состояний экрана равное четырем:


  • пустой список;
  • список с только что добавленным продуктом;
  • список с одним не выбранным продуктом;
  • список с одним выбранным продуктом.

Composable Architecture реализует подход data-driven development, что значительно облегчает написание snapshot-тестов конфигурация UI определяется текущим состоянием системы.


Приступим:


import XCTestimport ComposableArchitecture// Подключаем библиотеку для снепшот тестированияimport SnapshotTesting@testable import Composableclass ShoppingListSnapshotTests: XCTestCase {    func testEmptyList() {        // Создаем view        let listView = ShoppingListView(            // создаем систему            store: ShoppingListStore(                // устанавливаем состояние                initialState: ShoppingListState(products: []),                reducer: Reducer { _, _, _ in .none },                environment: ShoppingListEnviroment.mock            )        )        assertSnapshot(matching: listView, as: .image)    }    func testNewItem() {        let listView = ShoppingListView(            // Чтобы не создавать store каждый раз             // можно завести экстеншен Store.mock(state:State)            store: .mock(state: ShoppingListState(                products: [Product(id: .mock, name: "", isInBox: false)]            ))        )        assertSnapshot(matching: listView, as: .image)    }    func testSingleItem() {        let listView = ShoppingListView(            store: .mock(state: ShoppingListState(                products: [Product(id: .mock, name: "Milk", isInBox: false)]            ))        )        assertSnapshot(matching: listView, as: .image)    }    func testCompleteItem() {        let listView = ShoppingListView(            store: .mock(state: ShoppingListState(                products: [Product(id: .mock, name: "Milk", isInBox: true)]            ))        )        assertSnapshot(matching: listView, as: .image)    }}

После выполнения всех тестов получаем набор эталонных значений:


image


В результате для каждого состояния системы было зафиксировано его визуальное представление.


Debug mode вишенка на торте


Для отладки работы редьюсера есть полезный инструмент debug:


Reducer { state, action, env in    switch action { ... }}.debug()// илиReducer { state, action, env in    switch action { ... }}.debugActions()

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


received action:  ShoppingListAction.load  (No state changes)received action:  ShoppingListAction.setupProducts(    [      Product(        id: 9F047826-B431-4D20-9B80-CC65D6A1101B,        name: "",        isInBox: false      ),      Product(        id: D9834386-75BC-4B9C-B87B-121FFFDB2F93,        name: "Tesggggg",        isInBox: false      ),      Product(        id: D4405C13-2BB9-4CD4-A3A2-8289EAC6678C,        name: "",        isInBox: false      ),    ]  ) ShoppingListState(   products: [+     Product(+       id: 9F047826-B431-4D20-9B80-CC65D6A1101B,+       name: "",+       isInBox: false+     ),+     Product(+       id: D9834386-75BC-4B9C-B87B-121FFFDB2F93,+       name: "Tesggggg",+       isInBox: false+     ),+     Product(+       id: D4405C13-2BB9-4CD4-A3A2-8289EAC6678C,+       name: "",+       isInBox: false+     ),   ] )

*плюсом отмечается изменения состояния системы.


Смотри в следующей серии


Часть 3 расширяем функционал, добавляем удаление и сортировку продуктов (in progress)


Часть 4 добавляем кэширование списка и идем в магазин (in progress)


Источники


Список продуктов Часть 2: github.com


Портал авторов подхода: pointfree.co


Исходники Composable Architecture: https://github.com/pointfreeco/swift-composable-architecture


Исходники Snaphsot testing: github.com

Подробнее..

Как мы строили параллельные вселенные для нашего (и вашего) CICD пайплайна в Octopod

08.02.2021 18:21:29 | Автор: admin

Как мы строили параллельные вселенные для нашего (и вашего) CI/CD пайплайна в Octopod



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

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

  • Production основное рабочее окружение, куда попадают пользователи системы.
  • Pre-Production окружение для тестирования релиз-кандидатов (версий, которые будут использованы в production, если пройдут все этапы тестирования; их также называют RC), максимально схожее с production, где используются production доступы для интеграции с внешними сервисами. Цель тестирования на Pre-production получить достаточную уверенность в том, что на Production проблем не будет.
  • Staging окружение для черновой проверки, как правило тестирование последних изменений, по возможности использует тестовые интеграции со сторонними системами, может отличаться от Production, используется для проверки правильности реализации новых функциональностей.

Что и как нам хотелось улучшить


C Pre-production все достаточно понятно: туда последовательно попадают релиз-кандидаты, история релизов такая же, как на Production. Со Staging же есть нюансы:

  1. ОРГАНИЗАЦИОННЕ. Тестирование критических частей может потребовать задержки публикации новых изменений; изменения могут взаимодействовать непредсказуемым образом; отслеживание ошибок становится трудным из-за большого количества активности на сервере; иногда возникает путаница, что в какой версии реализовано; бывает непонятно, какое из накопившихся изменений вызвало проблему.
  2. СЛОЖНОСТЬ УПРАВЛЕНИЯ ОКРУЖЕНИЯМИ. Нужны разные окружения для тестирования разных изменений: для одной внешней системы может потребоваться production доступ, а работать с другой нужно в тестовой среде вместо боевой. А если staging один, то эти настройки распространяются на все реализованные фичи до следующего деплоймента. Приходится всё время об этом помнить и предостерегать сотрудников. Ситуация в целом похожа на работу многопоточного приложения с единым разделяемым ресурсом: он блокируется одними потребителями, остальные ждут. Например, один QA инженер ждет возможности проверки платежного шлюза с production интеграцией, пока другой проверяет все на интеграции тестовой.
  3. ДЕФЕКТ. Критичный дефект может заблокировать тестирование всех новых изменений разом

  4. ИЗМЕНЕНИЯ СХЕМ БД. Тяжело управлять изменениями схемы баз данных на одном стенде в периоды её активной разработки. Откати туда-сюда. Упс, тут не ревертится. А тут отревертили, но данные тестировщиков потеряли. Хочется для тестирования разных функциональностей иметь разные, изолированные друг от друга базы.
  5. УВЕЛИЧЕННОЕ ВРЕМЯ ПРИЁМКИ. Из-за того, что комбинация прошлых пунктов иногда приводит к ситуациям, когда часть фичей становится недоступна тестировщикам вовремя, или настройки окружения не позволяют приступить к тестированию сразу, происходят задержки на стороне разработки и тестирования. Допустим, в фиче обнаруживается дефект, и она возвращается на доработку разработчику, который уже вовсю занят другой задачей. Ему было бы удобнее получить её на доработку скорее, пока контекст задачи не потерян. Это приводит к увеличению отрезка времени от разработки до релиза в production для этих фич, что увеличивает так называемые time-to-production и time-to-market метрики.

Каждый из этих пунктов так или иначе решается, но все это привело к вопросу, а получится ли упростить себе жизнь, если мы уйдем от концепции одного staging-стенда к их динамическому количеству. Аналогично тому, как у нас есть проверки на CI для каждой ветки в git, мы можем получить и стенды для проверки QA для каждой ветки. Единственное, что нас останавливает от такого хода недостаток инфраструктуры и инструментов. Грубо говоря, для принятия фичи создается отдельный staging с выделенным доменным именем, QA тестирует его, принимает или возвращает на доработку. Примерно вот так:


Проблематика различных окружений при таком подходе решается естественным способом:


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

Наш путь к решению


Первое, что мы попробовали это прототип, собранный нашим DevOps: комбинация из docker-compose (оркестрация), rundeck (менеджмент) и portainer (интроспекция), которая позволила протестировать общее направление мысли и подход. С удобством были проблемы:

  1. Для любого изменения требовался доступ к коду и rundeck, которые были у разработчиков, но их не было, например, у QA инженеров.
  2. Поднято это было на одной большой машине, которой вскоре стало недостаточно, и для следующего шага уже был нужен Kubernetes или что-то аналогичное.
  3. Portainer давал информацию не о состоянии конкретного staging, а о наборе контейнеров.
  4. Приходилось постоянно мерджить файлик с описанием стейджингов, старые стенды надо было удалять.

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

  1. Использовать Kubernetes, чтобы масштабироваться на любое количество staging-окружений и иметь стандартный для современного DevOps набор инструментов.
  2. Решение, которое было бы просто интегрировать в инфраструктуру, уже использующую Kubernetes.
  3. Простой и удобный графический интерфейс для сотрудников на таких ролях как Руководители проектов, Product менеджеры и QA-инженеры. Они могут не работать с кодом напрямую, но инструменты для интроспекции и возможности задеплоить новый стейджинг у них должны быть. Результат не дергаем разработчиков на каждый чих.
  4. Решение, которое удобно интегрируется со стандартными CI/CD пайплайнами, чтобы его можно было использовать в разных проектах. Мы начали с проекта, который использует Github Actions как CI.
  5. Оркестрацию, детали которой сможет гибко настраивать DevOps инженер.
  6. Возможность скрывать логи и внутренние детали кластера в графическом интерфейсе, если проектная команда не хочет, чтобы они были доступны всем и/или есть какие-то опасения на этот счет.
  7. Полная информация и список действий должны быть доступны суперпользователям в лице DevOps инженеров и тимлидов.

И мы приступили к разработке Octopod. Названием послужило смешение нескольких мыслей про K8S, который мы использовали для оркестрации всего на проекте: множество проектов в этой экосистеме отражает морскую эстетику и тематику, а нам представлялся эдакий осьминог, щупальцами оркестрирующий множество подводных контейнеров. К тому же Pod один из основополагающих объектов в Kubernetes.

По техническому стеку Octopod представляет из себя Haskell, Rust, FRP, компиляцию в JS, Nix. Но вообще рассказ не об этом, поэтому я подробнее на этом останавливаться не буду.

Новая модель стала называться Multi-staging внутри нашей компании. Эксплуатация одновременно нескольких staging окружений сродни путешествиям по параллельным вселенным и измерениям в научной (и не очень) фантастике. В ней вселенные похожи друг на друга за исключением одной маленькой детали: где-то разные стороны победили в войне, где-то случилась культурная революция, где-то технологический прорыв. Предпосылка может быть и небольшой, но к каким изменениям она может привести! В наших же процессах эта предпосылка содержимое каждой отдельно взятой feature-ветки.


Внедрение у нас происходило в несколько этапов и начиналось с одного проекта. Оно включает в себя и подгонку оркестрации проекта со стороны DevOps и реорганизацию процесса тестирования и коммуникации со стороны руководителя проекта.

В результате ряда итераций некоторые фичи самого Octopod были удалены или изменены до неузнаваемости. Например, у нас в первой версии была страница с логом деплоя для каждого контура, но вот незадача не в каждой команде приемлемо то, что credentials могут через эти логи протекать ко всем сотрудникам, задействованным в разработке. В итоге мы решили избавиться от этой функциональности, а потом вернули её в другом виде теперь это настраиваемо (а поэтому опционально) и реализовано через интеграцию с kubernetes dashboard.

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

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

В результате


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

Самым сложным, как водится, было первое внедрение на нем выловили большую часть технических вопросов и проблем. Фидбек от пользователей позволил лучше понять, что требует доработки в первую очередь. В последних версиях интерфейс и работа с Octopod выглядит примерно так:


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

Open-source


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

Весь исходный код с примерами настройки и документацией доступен в репозитории на Github.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru