Рубрики
Без рубрики

Java с мышлением Clojure

Видео, стенограмма и слайды с выступлений Java2Days и Oredev 2018. Помеченный clojure, java, разговоры.

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

Сначала дразнилка:

Вот слайды в формате keynote , формат ppt (непроверенный!) и Слайд-шоу (каким-то образом сломано!) .

И видео:

Отзывы и вопросы приветствуются!

Давайте начнем с викторины. Кто может сказать мне, что это такое?

Вот как выглядит Clojure, когда вы видите его в первый раз.

И я хочу отметить 3 важные вещи на этой картинке.

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

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

Или что это, черт возьми, такое и что оно там делает?

Но последнее, и самое главное, это чужеродно. И когда мы смотрим на эту картину, мы смотрим на нее с нашей человеческой точки зрения, с нашей точки зрения Java.

Так что, возможно, мы поступаем несправедливо, и у нас достаточно времени,

и с помощью гипнотода, может быть, эта картинка выглядела бы так:

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

И я знаю, о чем вы думаете, Чудо-женщина технически не инопланетянка, и если мы говорим о Clojure, картинка отсутствует

несколько скобок.

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

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

Итак, что мы собираемся сделать на этом занятии, так это на примере реального приложения посмотреть, как Clojure повлияла на то, как мы создавали, как мы разрабатывали это Java-приложение, как Clojure заразила наш Java-код.

Чтобы дать немного больше контекста, когда я начал работать с этим приложением,

Я уже 12 лет был разработчиком Java, а 3 года – разработчиком Clojure, поэтому, когда я начал участвовать, я достиг 2-й стадии Wikus в своем путешествии, в своей трансформации, в полноценный инопланетный разум.

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

Что-то вроде “если вы положите немного денег на свой счет, мы дадим вам вдвое больше этой суммы бесплатно” или “если вы присоединитесь к нам сейчас, мы дадим вам 1000 евро бесплатно наличными!”.

Теперь никто не собирается давать вам никаких бесплатных наличных, поэтому, если вы прочитали T & C, чтобы иметь право снять эти “бесплатные” наличные, чтобы иметь возможность получить эти наличные и положить их в свой карман, вам нужно было сначала сыграть или сделать ставку, несколько раз в системе, или выполнить ряд действий.

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

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

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

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

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

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

Чистые Функции

Это концепция чистых функций.

Чистые функции – это фрагменты кода, которые для заданного ввода всегда будут возвращать один и тот же результат. Всегда. Так что тот же вход, тот же выход.

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

Чистые функции подобны физическим законам,

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

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

А перемены – это то, чем мы, разработчики, зарабатываем на жизнь.

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

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

Побочные эффекты

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

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

Контекст, который вам нужно держать в голове, огромен.

Как только у вас появятся побочные эффекты,

вы не уверены, что произойдет, когда вы внесете изменения.

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

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

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

В основном существует два вида побочных эффектов:

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

Государство

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

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

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

Таким образом, это был бы наш объектный график, и у нас был бы один из этих графиков на каждого клиента.

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

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

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

Таким образом, код смешивает в одном и том же месте бизнес-правила и правила параллелизма.

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

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

Для части управления состоянием Clojure поставляется с конструктором класса a, называемым Atom, который в основном такой же, как Java AtomicReference, но с немного большей функциональностью.

атом =~ j.u.c.a. Атомарная ссылка

Давайте посмотрим, как работает атом.

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

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

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

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

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

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

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

Другое дело, что если бы, например, существовал другой поток, поток-3, который в момент времени 0 считывал текущее состояние атома и сохранял ссылку на него в течение некоторого времени, с течением времени, от t0 до t1 и t2, поток-3 все равно увидит начальное состояние, белое состояние. Поскольку значение является неизменяемым, никто не может к нему прикоснуться, а это означает, что поток-3 потенциально может работать с устаревшим значением.

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

Допустим, у вас есть это состояние,

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

Эти 4 вещи – вот в чем разница между зеленым и белым состоянием. Таким образом, чтобы создать зеленое состояние, вам просто нужно создать 4 новых объекта, и вы можете повторно использовать все остальные, и вы можете сделать это, потому что все эти объекты неизменяемы, и мы знаем, что делиться неизменяемыми объектами безопасно. Этот метод называется структурным разделением.

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

Является ли это потокобезопасным? Каждый разработчик Java, каждый день.

Вы когда-нибудь задавали себе этот вопрос? С неизменяемостью и атомами вы все еще задаете этот вопрос, но правила для ответа на него намного проще, поскольку они не связаны с моделью памяти Java и семантикой “происходит раньше”.

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

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

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

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

Так как все это повлияло на наш Java-код?

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

public class ClientBonus {

    private final Client client;
    private final Bonus bonus;
    private final DepositList deposits;

…

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

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

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

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

public interface ConcurrentMap<…> extends Map<…> {

V compute(K key, BiFunction<…> remappingFunction) 
V computeIfAbsent(K key, Function<…> mappingFunction) 
V computeIfPresent(K key, BiFunction<…> remappingFunction) 
…
}

Итак, вот как будет выглядеть класс, владеющий государством.

public class TheStateHolder {
    private final Map state = new ConcurrentHashMap<>();
    public ClientBonus nextState(Long client, Bet bet) {
        return state.computeIfPresent(
                client,
                (k, currentState) -> currentState.nextState(bet));
    }

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

В нашем случае мы решили, что сам Бонус Клиента должен быть тем, который вычисляет новое состояние,

public class ClientBonus {
...
    public ClientBonus nextState(Bet bet) {
        ...
    }
…

так что следующая функция состояния должна быть чистой функцией.

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

Эффекты

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

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

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

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

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

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

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

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

public class ClientBonus {
...
    public Pair next(Bet bet) {
        ...
    }
…

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

Это было бы примером класса, представляющего эффект уведомления клиента о ходе получения бонуса.

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

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

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

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

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

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

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

public interface Effect {
    void run(AllDependencies dependencies);
}

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

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

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

Итак, вот как будет выглядеть наш код:

Pair pair = theStateHolder.nextState(bet);
pair.effects.run(dependencies);

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

Но вопрос в том, является ли это потокобезопасным?

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

И где здесь условия гонки?

Итак, предположим, что приходят два потока и вычисляют новое состояние и выполняемые эффекты.

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

Но теперь, когда мы находимся за пределами механизма atom, мы подвержены возможным условиям гонки, так что может случиться так, что поток-2 выполнит свои побочные эффекты раньше потока-1.

И это может быть приемлемо, а может и не быть приемлемым, в зависимости от ваших бизнес-требований.

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

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

Агенты Clojure в основном похожи на атом, но это дает вам дополнительную гарантию однопоточности. Если вы знакомы с актерами, они немного похожи на актеров в своей модели параллелизма.

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

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

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

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

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

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

Обратите внимание, что это все еще были наши чистые функции, те, которые вычисляли эффекты,

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

Сопутствующие эффекты

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

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

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

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

Это в основном поиск событий. По своей сути, поиск событий и функциональное программирование имеют много общего.

Поиск событий и функциональное программирование идут рука об руку.

Выгоды

Итак, вот как все это выглядит, если собрать все воедино

public class KafkaConsumer {
    private AllDependencies allDependencies;
    private TheStateHolder theStateHolder;

    public void run() {
        while (!stop) {
            Bet bet = readNext();
            Effects effects = theStateHolder.event(bet);
            effects.run(allDependencies);
        }
    }
…
}

Это две зависимости, которые будут внедрены выбранной вами платформой внедрения зависимостей.

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

Здесь мы используем api опроса Кафки, поэтому потребителем Кафки будет поток, который будет считывать новые события из темы Кафки.

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

И последнее, мы просим эффекты выполнить сами.

Следуя этому подходу, с нашим кодом происходят некоторые интересные вещи:

Во-первых, у наших бизнес-объектов было 0 геттеров или сеттеров.

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

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

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

Функциональное ядро, Императивная оболочка

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

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

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

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

Поскольку существуют и другие архитектуры, которые имеют такую же круглую форму, я хочу кое-что прояснить.

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

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

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

Вот как выглядит типичная программа Clojure:

clientBonus = Map.of(
        "client", Map.of("id", "123233"),
        "deposits",
        List.of(
                Map.of("amount", 3,
                            "type", "CASH"),
                Map.of("amount", 234,
                            "type", "CARD")));

((List) clientBonus.get("deposits"))
        .stream()
        .collect(
                Collectors.summarizingInt(
                        m -> (int) ((Map) m).get("amount")));

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

Я не уверен, что вы думаете об этом, но для моей чувствительности к Java,

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

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

Но когда вы пишете код Clojure, удивительно, что эта динамическая типизация является меньшей проблемой, и я думаю, что это связано с тем, что api ядра Clojure адаптирован для работы с этими динамическими структурами данных, поэтому намного меньше хлопот, чем с Java API.

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

Так когда вы вводите этот класс,

public class Bet {    
    private String id;   
    private int amount;
    private long timestamp;
}

вы начинаете задаваться вопросом, а в чем же ценность создания нового класса? Что я получу от этого вместо использования простой карты?

И вы замечаете, что первое, что вы получаете, – это почти бесполезный метод toString, вы также получаете сломанную реализацию equals и hashCode. Это действительно раздражает, но, по крайней мере, у нас есть Ломбок.

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

Более того, сколько библиотек вы собираетесь найти в Github, которые работают с этим новым классом? Никто.

Именно в этот момент вы начинаете понимать, что Алан Перлис имел в виду, говоря

Лучше, чтобы 100 функций работали с одной структурой данных, чем 10 функций с 10 структурами данных. Алан Перлис

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

Но что, если мы просто оставим наши лучшие объекты в виде простых данных?

{:type :bet
 :id "client1"
 :amount 23
 :timestamp 123312321323}

У вас есть разумная строка, та, которую вы видите там.

Вы также получаете правильный пароль и хэш-код бесплатно.

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

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

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

{:request-method :get

 :uri            "/foobaz"
 :query-params   {"somekey" "somevalue"}
 :headers        {"accept-encoding" "gzip, deflate" 
                  "connection" "close"}
 :body           nil
 :scheme         :http

 :content-length 0
 :server-port    8080
 :server-name    "localhost"}

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

{:status  200
 
 :headers {"Content-Type" "text/html"} 
 :body    "Hello World"}

Подумайте, насколько проще стал бы ваш тест.

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

Sql-запросы:

{:select [:id :client :amount]
 :from   [:transactions]
 :where  [:= :client "a"]}

и наборы результатов базы данных:

[{:id 1 :client 32 :amount 3} 
 {:id 2 :client 87 :amount 7} 
 {:id 3 :client 32 :amount 4} 
 {:id 4 :client 40 :amount 6}]

HTML и CSS:

[:html
    
 [:body
        
  [:p "Count: 4"]
  [:p "Total: 20"]]]

Конфигурация:

{:web-server          {:listen 8080} 
 :db-config           {:host     "xxxx"
                       
                       :user     "xxxx"
                       
                       :password "xxxx"} 
 :http-defaults       {:connection-timeout 10000                      
                       :request-timeout    10000                       
                       :max-connections    2000} 
 :user-service        {:url "http://user-service"                       
                       :connection-timeout 1000}}

Даже данные о ваших данных, ваши метаданные:

{:id        :string
 
 :name      :string
 
 :deposits  [{:id        :string
              
              :amount    :int
              
              :timestamp :long}]}

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

  1. ваша бизнес-логика
  2. ваш код инфраструктуры
  3. ваша конфигурация
  4. ваши метаданные.

Всего один API, который вам нужно изучить и освоить.

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

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

Вы делаете это с помощью ОТВЕТА.

Конечно, в Java теперь есть что-то, что можно назвать ОТВЕТОМ, но

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

При правильном ОТВЕТЕ вы никогда не создаете и не запускаете свое приложение, вы развиваете приложение изнутри, по чуть-чуть за раз.

Правильный ОТВЕТ дает вам те же ощущения, ту же эргономику, что и оболочка Unix.

Правильный ОТВЕТ – это все равно, что иметь отладчик, постоянно подключенный к запущенной JVM.

Правильный ОТВЕТ – это недостающая часть вашего рабочего процесса разработки, основанного на тестировании.

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

Правильный ОТВЕТ – это то, чего мне больше всего не хватает при работе с Java.

Хорошо, последняя часть выступления.

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

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

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

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

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

Но я знаю, о чем ты думаешь

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

.filter(removeCsvHeaders(firstHeader))
.map(splitCsvString())
.map(convertCsvToMap(csvHeaders))
.map(convertToJson(eventCreator))
(filter not-header?)
(map parse-csv-line)
(map (partial zipmap headers))
(map ->event)

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

Давайте посчитаем скобки. 1, 2, 3 … Версия Java содержит 16 скобок. И сколько их у Clojure one? 10. Таким образом, версия Clojure содержит на 40% меньше скобок.

Но не только это, версия приложения Clojure содержала одну десятую часть кода.

Одна десятая, представьте, что вы могли бы удалить 90% своего кода.

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

List.of(
        new Symbol("defn"),
        new Symbol("plus-one"),
        List.of(
                new Symbol("a"),
                new Symbol("b")),
        Map.of(
                new Keyword("time"), List.of(new Symbol("System/currentTimeMillis")),
                new Keyword("result"), List.of(
                        new Symbol("+"),
                        new Symbol("a"),
                        new Symbol("b"),
                        new Long(1))));

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

Хорошо, может быть, Clojure немного менее подробен, чем этот, но это, по сути, то, что вы делаете, когда пишете, когда вводите Clojure. Что это? Ваш код – это просто списки и карты, вот что мы имеем в виду, когда говорим, что в Lisp код – это данные, потому что посмотрите на это, это реальные данные.

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

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

Вот почему Шептуны так любят свои скобки.

Итак, вкратце…

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

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

Но после того, как я насладился динамичным опытом разработки Clojure, это то, что я бы никогда не хотел отпускать.

И, пожалуйста, не бойтесь скобок. Точно так же, как вы не написали бы Java без IDE, вы не напишете Clojure без нее. И ИДЕЯ состоит в том, чтобы позаботиться обо всех этих страшных скобках. И помните, что для них есть очень веская и веская причина.

Я хочу закончить еще одной цитатой Алана Перлиса:

Язык, который не влияет на то, как вы думаете о программировании, не стоит знать.

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

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

Но ни один из них не был самым важным уроком Clojure.

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

Если бы кто-нибудь из вас сказал мне 5 или 6 лет назад выучить динамический шепелявый, я бы сказал: “Ни в коем случае, я не собираюсь тратить свое время впустую”. И все же я здесь, проповедую о Clojure.

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

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

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

Я уверен, что в этом путешествии вы научитесь чему-то, что захотите привнести в свою повседневную работу.

И в худшем случае,

это просто сделает вас более странным и с вами будет сложнее общаться.

Большое спасибо, что уделили мне время.

Но прежде чем вы уйдете, пожалуйста, бегло взгляните на это видео:

Оригинал: “https://dev.to/danlebrero/java-with-a-clojure-mindset-1p13”